Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-11-20 01:11:55 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2019-11-20 01:11:55 +0300
commit5a8431feceba47fd8e1804d9aa1b1730606b71d5 (patch)
treee5df8e0ceee60f4af8093f5c4c2f934b8abced05 /lib
parent4d477238500c347c6553d335d920bedfc5a46869 (diff)
Add latest changes from gitlab-org/gitlab@12-5-stable-ee
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/commits.rb6
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/entities.rb66
-rw-r--r--lib/api/group_clusters.rb1
-rw-r--r--lib/api/group_container_repositories.rb4
-rw-r--r--lib/api/group_export.rb34
-rw-r--r--lib/api/helpers.rb17
-rw-r--r--lib/api/helpers/internal_helpers.rb3
-rw-r--r--lib/api/helpers/pagination.rb249
-rw-r--r--lib/api/helpers/projects_helpers.rb3
-rw-r--r--lib/api/internal/base.rb2
-rw-r--r--lib/api/merge_requests.rb12
-rw-r--r--lib/api/pages_domains.rb14
-rw-r--r--lib/api/project_clusters.rb1
-rw-r--r--lib/api/project_container_repositories.rb11
-rw-r--r--lib/api/projects.rb5
-rw-r--r--lib/api/releases.rb2
-rw-r--r--lib/api/settings.rb15
-rw-r--r--lib/api/sidekiq_metrics.rb3
-rw-r--r--lib/banzai/filter/inline_grafana_metrics_filter.rb76
-rw-r--r--lib/banzai/filter/inline_metrics_redactor_filter.rb89
-rw-r--r--lib/banzai/filter/video_link_filter.rb2
-rw-r--r--lib/banzai/pipeline/ascii_doc_pipeline.rb3
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/bitbucket/representation/pull_request.rb5
-rw-r--r--lib/container_registry/client.rb2
-rw-r--r--lib/declarative_policy.rb9
-rw-r--r--lib/feature/gitaly.rb3
-rw-r--r--lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb2
-rw-r--r--lib/gitlab.rb12
-rw-r--r--lib/gitlab/analytics/cycle_analytics/base_query_builder.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/data_collector.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/records_fetcher.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events.rb18
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb8
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb4
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb4
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb8
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb8
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb8
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/metrics_based_stage_event.rb17
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb5
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/simple_stage_event.rb13
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb4
-rw-r--r--lib/gitlab/auth/ip_rate_limiter.rb19
-rw-r--r--lib/gitlab/auth/ldap/config.rb8
-rw-r--r--lib/gitlab/background_migration/legacy_upload_mover.rb1
-rw-r--r--lib/gitlab/background_migration/legacy_uploads_migrator.rb2
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb2
-rw-r--r--lib/gitlab/ci/ansi2json/converter.rb101
-rw-r--r--lib/gitlab/ci/ansi2json/line.rb7
-rw-r--r--lib/gitlab/ci/ansi2json/state.rb4
-rw-r--r--lib/gitlab/ci/ansi2json/style.rb21
-rw-r--r--lib/gitlab/ci/build/context/base.rb35
-rw-r--r--lib/gitlab/ci/build/context/build.rb41
-rw-r--r--lib/gitlab/ci/build/context/global.rb41
-rw-r--r--lib/gitlab/ci/build/policy/changes.rb2
-rw-r--r--lib/gitlab/ci/build/policy/kubernetes.rb2
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb2
-rw-r--r--lib/gitlab/ci/build/policy/specification.rb2
-rw-r--r--lib/gitlab/ci/build/policy/variables.rb4
-rw-r--r--lib/gitlab/ci/build/rules.rb14
-rw-r--r--lib/gitlab/ci/build/rules/rule.rb4
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause.rb2
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/changes.rb2
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/exists.rb2
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/if.rb7
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb17
-rw-r--r--lib/gitlab/ci/config/entry/boolean.rb20
-rw-r--r--lib/gitlab/ci/config/entry/commands.rb4
-rw-r--r--lib/gitlab/ci/config/entry/default.rb34
-rw-r--r--lib/gitlab/ci/config/entry/files.rb26
-rw-r--r--lib/gitlab/ci/config/entry/job.rb67
-rw-r--r--lib/gitlab/ci/config/entry/key.rb45
-rw-r--r--lib/gitlab/ci/config/entry/need.rb44
-rw-r--r--lib/gitlab/ci/config/entry/needs.rb55
-rw-r--r--lib/gitlab/ci/config/entry/prefix.rb20
-rw-r--r--lib/gitlab/ci/config/entry/root.rb5
-rw-r--r--lib/gitlab/ci/config/entry/rules/rule.rb15
-rw-r--r--lib/gitlab/ci/config/entry/script.rb6
-rw-r--r--lib/gitlab/ci/config/entry/workflow.rb25
-rw-r--r--lib/gitlab/ci/config/normalizer.rb20
-rw-r--r--lib/gitlab/ci/pipeline/chain/base.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content.rb59
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/process.rb41
-rw-r--r--lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb50
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb21
-rw-r--r--lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb6
-rw-r--r--lib/gitlab/ci/pipeline/chain/seed.rb64
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/config.rb33
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb47
-rw-r--r--lib/gitlab/ci/pipeline/seed/build/cache.rb77
-rw-r--r--lib/gitlab/ci/status/build/failed.rb4
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml13
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml67
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml69
-rw-r--r--lib/gitlab/ci/yaml_processor.rb46
-rw-r--r--lib/gitlab/cleanup/orphan_job_artifact_files.rb11
-rw-r--r--lib/gitlab/cluster/lifecycle_events.rb67
-rw-r--r--lib/gitlab/cluster/mixins/puma_cluster.rb6
-rw-r--r--lib/gitlab/cluster/mixins/unicorn_http_server.rb19
-rw-r--r--lib/gitlab/cluster/puma_worker_killer_initializer.rb13
-rw-r--r--lib/gitlab/config/entry/configurable.rb29
-rw-r--r--lib/gitlab/config/entry/factory.rb20
-rw-r--r--lib/gitlab/config/entry/inheritable.rb40
-rw-r--r--lib/gitlab/config/entry/node.rb4
-rw-r--r--lib/gitlab/config/entry/simplifiable.rb11
-rw-r--r--lib/gitlab/config/entry/validatable.rb21
-rw-r--r--lib/gitlab/config/entry/validators.rb39
-rw-r--r--lib/gitlab/cycle_analytics/group_stage_summary.rb7
-rw-r--r--lib/gitlab/cycle_analytics/summary/group/base.rb5
-rw-r--r--lib/gitlab/cycle_analytics/summary/group/deploy.rb3
-rw-r--r--lib/gitlab/cycle_analytics/summary/group/issue.rb16
-rw-r--r--lib/gitlab/daemon.rb9
-rw-r--r--lib/gitlab/danger/helper.rb16
-rw-r--r--lib/gitlab/danger/teammate.rb5
-rw-r--r--lib/gitlab/data_builder/deployment.rb8
-rw-r--r--lib/gitlab/data_builder/push.rb30
-rw-r--r--lib/gitlab/database/migration_helpers.rb17
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb10
-rw-r--r--lib/gitlab/database_importers/self_monitoring/project/create_service.rb53
-rw-r--r--lib/gitlab/devise_failure.rb8
-rw-r--r--lib/gitlab/error_tracking/detailed_error.rb31
-rw-r--r--lib/gitlab/error_tracking/error_event.rb11
-rw-r--r--lib/gitlab/etag_caching/router.rb4
-rw-r--r--lib/gitlab/experimentation.rb69
-rw-r--r--lib/gitlab/favicon.rb2
-rw-r--r--lib/gitlab/file_finder.rb17
-rw-r--r--lib/gitlab/git/commit.rb4
-rw-r--r--lib/gitlab/git/repository.rb11
-rw-r--r--lib/gitlab/git/wiki.rb8
-rw-r--r--lib/gitlab/git_access_result/custom_action.rb6
-rw-r--r--lib/gitlab/gitaly_client.rb27
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb45
-rw-r--r--lib/gitlab/gitaly_client/namespace_service.rb19
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb12
-rw-r--r--lib/gitlab/gon_helper.rb3
-rw-r--r--lib/gitlab/gpg.rb49
-rw-r--r--lib/gitlab/grape_logging/loggers/exception_logger.rb33
-rw-r--r--lib/gitlab/graphql/authorize/instrumentation.rb6
-rw-r--r--lib/gitlab/graphql/connections.rb6
-rw-r--r--lib/gitlab/graphql/connections/filterable_array_connection.rb17
-rw-r--r--lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb40
-rw-r--r--lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb57
-rw-r--r--lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb41
-rw-r--r--lib/gitlab/graphql/connections/keyset/connection.rb153
-rw-r--r--lib/gitlab/graphql/connections/keyset/legacy_keyset_connection.rb66
-rw-r--r--lib/gitlab/graphql/connections/keyset/order_info.rb78
-rw-r--r--lib/gitlab/graphql/connections/keyset/query_builder.rb68
-rw-r--r--lib/gitlab/graphql/connections/keyset_connection.rb85
-rw-r--r--lib/gitlab/graphql/filterable_array.rb14
-rw-r--r--lib/gitlab/graphql/loaders/pipeline_for_sha_loader.rb25
-rw-r--r--lib/gitlab/health_checks/master_check.rb66
-rw-r--r--lib/gitlab/import_export.rb14
-rw-r--r--lib/gitlab/import_export/config.rb5
-rw-r--r--lib/gitlab/import_export/file_importer.rb2
-rw-r--r--lib/gitlab/import_export/group_import_export.yml36
-rw-r--r--lib/gitlab/import_export/group_project_object_builder.rb11
-rw-r--r--lib/gitlab/import_export/group_tree_saver.rb55
-rw-r--r--lib/gitlab/import_export/import_export.yml3
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb206
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb31
-rw-r--r--lib/gitlab/import_export/reader.rb27
-rw-r--r--lib/gitlab/import_export/relation_factory.rb15
-rw-r--r--lib/gitlab/import_export/relation_rename_service.rb2
-rw-r--r--lib/gitlab/import_export/relation_tree_saver.rb27
-rw-r--r--lib/gitlab/import_export/saver.rb18
-rw-r--r--lib/gitlab/import_export/shared.rb40
-rw-r--r--lib/gitlab/instrumentation_helper.rb44
-rw-r--r--lib/gitlab/kubernetes/config_maps/aws_node_auth.rb46
-rw-r--r--lib/gitlab/kubernetes/helm.rb4
-rw-r--r--lib/gitlab/kubernetes/helm/install_command.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/errors.rb5
-rw-r--r--lib/gitlab/metrics/dashboard/finder.rb3
-rw-r--r--lib/gitlab/metrics/dashboard/processor.rb3
-rw-r--r--lib/gitlab/metrics/dashboard/service_selector.rb5
-rw-r--r--lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb224
-rw-r--r--lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/url.rb46
-rw-r--r--lib/gitlab/metrics/exporter/web_exporter.rb25
-rw-r--r--lib/gitlab/metrics/requests_rack_middleware.rb4
-rw-r--r--lib/gitlab/pagination/base.rb32
-rw-r--r--lib/gitlab/pagination/offset_pagination.rb77
-rw-r--r--lib/gitlab/project_authorizations.rb30
-rw-r--r--lib/gitlab/project_template.rb3
-rw-r--r--lib/gitlab/prometheus/internal.rb45
-rw-r--r--lib/gitlab/prometheus/metric_group.rb16
-rw-r--r--lib/gitlab/prometheus/queries/knative_invocation_query.rb13
-rw-r--r--lib/gitlab/quick_actions/issuable_actions.rb2
-rw-r--r--lib/gitlab/quick_actions/issue_actions.rb22
-rw-r--r--lib/gitlab/redis/wrapper.rb2
-rw-r--r--lib/gitlab/regex.rb15
-rw-r--r--lib/gitlab/search/found_blob.rb54
-rw-r--r--lib/gitlab/seeder.rb65
-rw-r--r--lib/gitlab/serializer/pagination.rb5
-rw-r--r--lib/gitlab/setup_helper.rb5
-rw-r--r--lib/gitlab/shell.rb12
-rw-r--r--lib/gitlab/sidekiq_daemon/monitor.rb6
-rw-r--r--lib/gitlab/sidekiq_logging/structured_logger.rb11
-rw-r--r--lib/gitlab/sidekiq_middleware/metrics.rb47
-rw-r--r--lib/gitlab/slash_commands/command.rb1
-rw-r--r--lib/gitlab/slash_commands/issue_comment.rb55
-rw-r--r--lib/gitlab/slash_commands/presenters/access.rb4
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_comment.rb43
-rw-r--r--lib/gitlab/slash_commands/presenters/note_base.rb48
-rw-r--r--lib/gitlab/sourcegraph.rb26
-rw-r--r--lib/gitlab/sql/union.rb2
-rw-r--r--lib/gitlab/task_helpers.rb18
-rw-r--r--lib/gitlab/tracking.rb7
-rw-r--r--lib/gitlab/usage_data.rb24
-rw-r--r--lib/gitlab/usage_data_counters/web_ide_counter.rb14
-rw-r--r--lib/gitlab/utils/deep_size.rb4
-rw-r--r--lib/gitlab/wiki_file_finder.rb6
-rw-r--r--lib/gitlab/workhorse.rb1
-rw-r--r--lib/google_api/cloud_platform/client.rb4
-rw-r--r--lib/grafana/client.rb14
-rw-r--r--lib/prometheus/pid_provider.rb10
-rw-r--r--lib/quality/kubernetes_client.rb14
-rw-r--r--lib/sentry/client.rb122
-rw-r--r--lib/tasks/dev.rake4
-rw-r--r--lib/tasks/gitlab/graphql.rake42
-rw-r--r--lib/tasks/gitlab/seed.rake2
-rw-r--r--lib/tasks/gitlab/shell.rake2
-rw-r--r--lib/tasks/gitlab/uploads/legacy.rake2
237 files changed, 4050 insertions, 1446 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index d71f0c38ce6..a2bdb76b834 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -21,6 +21,7 @@ module API
Gitlab::GrapeLogging::Loggers::ClientEnvLogger.new,
Gitlab::GrapeLogging::Loggers::RouteLogger.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new,
+ Gitlab::GrapeLogging::Loggers::ExceptionLogger.new,
Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new,
Gitlab::GrapeLogging::Loggers::PerfLogger.new,
Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new
@@ -112,6 +113,7 @@ module API
mount ::API::Files
mount ::API::GroupBoards
mount ::API::GroupClusters
+ mount ::API::GroupExport
mount ::API::GroupLabels
mount ::API::GroupMilestones
mount ::API::Groups
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index f8f79ab6f5a..054242dca4c 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -32,7 +32,7 @@ module API
use :filter_params
end
get ':id/repository/branches' do
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42329')
+ user_project.preload_protected_branches
repository = user_project.repository
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index ffff40141de..63a7fdfa3ab 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -169,7 +169,7 @@ module API
not_found! 'Commit' unless commit
- raw_diffs = ::Kaminari.paginate_array(commit.raw_diffs.to_a)
+ raw_diffs = ::Kaminari.paginate_array(commit.diffs(expanded: true).diffs.to_a)
present paginate(raw_diffs), with: Entities::Diff
end
@@ -223,7 +223,7 @@ module API
present user_project.repository.commit(result[:result]),
with: Entities::Commit
else
- render_api_error!(result[:message], 400)
+ error!(result.slice(:message, :error_code), 400, header)
end
end
@@ -257,7 +257,7 @@ module API
present user_project.repository.commit(result[:result]),
with: Entities::Commit
else
- render_api_error!(result[:message], 400)
+ error!(result.slice(:message, :error_code), 400, header)
end
end
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index da882547071..f97200f20b9 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -17,7 +17,7 @@ module API
end
params do
use :pagination
- optional :order_by, type: String, values: %w[id iid created_at ref], default: 'id', desc: 'Return deployments ordered by `id` or `iid` or `created_at` or `ref`'
+ optional :order_by, type: String, values: %w[id iid created_at updated_at ref], default: 'id', desc: 'Return deployments ordered by `id` or `iid` or `created_at` or `updated_at` or `ref`'
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 91811efacd7..9617f1a8acf 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -307,6 +307,7 @@ module API
expose :only_allow_merge_if_pipeline_succeeds
expose :request_access_enabled
expose :only_allow_merge_if_all_discussions_are_resolved
+ expose :remove_source_branch_after_merge
expose :printing_merge_request_link_enabled
expose :merge_method
expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) {
@@ -488,11 +489,11 @@ module API
end
expose :developers_can_push do |repo_branch, options|
- options[:project].protected_branches.developers_can?(:push, repo_branch.name)
+ ::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches)
end
expose :developers_can_merge do |repo_branch, options|
- options[:project].protected_branches.developers_can?(:merge, repo_branch.name)
+ ::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches)
end
expose :can_push do |repo_branch, options|
@@ -754,6 +755,7 @@ module API
end
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
+ expose :squash_commit_sha
expose :discussion_locked
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
@@ -776,6 +778,10 @@ module API
expose :squash
expose :task_completion_status
+
+ expose :cannot_be_merged?, as: :has_conflicts
+
+ expose :mergeable_discussions_state?, as: :blocking_discussions_resolved
end
class MergeRequest < MergeRequestBasic
@@ -1248,6 +1254,7 @@ module API
# let's not expose the secret key in a response
attributes.delete(:asset_proxy_secret_key)
+ attributes.delete(:eks_secret_access_key)
attributes
end
@@ -1290,7 +1297,11 @@ module API
end
class Release < Grape::Entity
- expose :name
+ include ::API::Helpers::Presentable
+
+ expose :name do |release, _|
+ can_download_code? ? release.name : "Release-#{release.id}"
+ end
expose :tag, as: :tag_name, if: ->(_, _) { can_download_code? }
expose :description
expose :description_html do |entity|
@@ -1302,8 +1313,8 @@ module API
expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? }
expose :upcoming_release?, as: :upcoming_release
expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? }
- expose :commit_path, if: ->(_, _) { can_download_code? }
- expose :tag_path, if: ->(_, _) { can_download_code? }
+ expose :commit_path, expose_nil: false
+ expose :tag_path, expose_nil: false
expose :assets do
expose :assets_count, as: :count do |release, _|
assets_to_exclude = can_download_code? ? [] : [:sources]
@@ -1315,8 +1326,9 @@ module API
end
end
expose :_links do
- expose :merge_requests_url, if: -> (_) { release_mr_issue_urls_available? }
- expose :issues_url, if: -> (_) { release_mr_issue_urls_available? }
+ expose :merge_requests_url, expose_nil: false
+ expose :issues_url, expose_nil: false
+ expose :edit_url, expose_nil: false
end
private
@@ -1324,36 +1336,6 @@ module API
def can_download_code?
Ability.allowed?(options[:current_user], :download_code, object.project)
end
-
- def commit_path
- return unless object.commit
-
- Gitlab::Routing.url_helpers.project_commit_path(project, object.commit.id)
- end
-
- def tag_path
- Gitlab::Routing.url_helpers.project_tag_path(project, object.tag)
- end
-
- def merge_requests_url
- Gitlab::Routing.url_helpers.project_merge_requests_url(project, params_for_issues_and_mrs)
- end
-
- def issues_url
- Gitlab::Routing.url_helpers.project_issues_url(project, params_for_issues_and_mrs)
- end
-
- def params_for_issues_and_mrs
- { scope: 'all', state: 'opened', release_tag: object.tag }
- end
-
- def release_mr_issue_urls_available?
- ::Feature.enabled?(:release_mr_issue_urls, project)
- end
-
- def project
- @project ||= object.project
- end
end
class Tag < Grape::Entity
@@ -1699,6 +1681,7 @@ module API
expose :verified?, as: :verified
expose :verification_code, as: :verification_code
expose :enabled_until
+ expose :auto_ssl_enabled
expose :certificate,
as: :certificate_expiration,
@@ -1714,6 +1697,7 @@ module API
expose :verified?, as: :verified
expose :verification_code, as: :verification_code
expose :enabled_until
+ expose :auto_ssl_enabled
expose :certificate,
if: ->(pages_domain, _) { pages_domain.certificate? },
@@ -1737,7 +1721,12 @@ module API
class Blob < Grape::Entity
expose :basename
expose :data
- expose :filename
+ expose :path
+ # TODO: :filename was renamed to :path but both still return the full path,
+ # in the future we can only return the filename here without the leading
+ # directory path.
+ # https://gitlab.com/gitlab-org/gitlab/issues/34521
+ expose :filename, &:path
expose :id
expose :ref
expose :startline
@@ -1813,6 +1802,7 @@ module API
expose :user, using: Entities::UserBasic
expose :platform_kubernetes, using: Entities::Platform::Kubernetes
expose :provider_gcp, using: Entities::Provider::Gcp
+ expose :management_project, using: Entities::ProjectIdentity
end
class ClusterProject < Cluster
diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb
index a70ac63cc6e..abfe10b7fa1 100644
--- a/lib/api/group_clusters.rb
+++ b/lib/api/group_clusters.rb
@@ -84,6 +84,7 @@ module API
requires :cluster_id, type: Integer, desc: 'The cluster ID'
optional :name, type: String, desc: 'Cluster name'
optional :domain, type: String, desc: 'Cluster base domain'
+ optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
optional :token, type: String, desc: 'Token to authenticate against Kubernetes'
diff --git a/lib/api/group_container_repositories.rb b/lib/api/group_container_repositories.rb
index fd24662cc9a..7f95b411b36 100644
--- a/lib/api/group_container_repositories.rb
+++ b/lib/api/group_container_repositories.rb
@@ -23,9 +23,11 @@ module API
end
get ':id/registry/repositories' do
repositories = ContainerRepositoriesFinder.new(
- id: user_group.id, container_type: :group
+ user: current_user, subject: user_group
).execute
+ track_event('list_repositories')
+
present paginate(repositories), with: Entities::ContainerRegistry::Repository, tags: params[:tags]
end
end
diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb
new file mode 100644
index 00000000000..8025a16e191
--- /dev/null
+++ b/lib/api/group_export.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module API
+ class GroupExport < Grape::API
+ before do
+ authorize! :admin_group, user_group
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups, requirements: { id: %r{[^/]+} } do
+ desc 'Download export' do
+ detail 'This feature was introduced in GitLab 12.5.'
+ end
+ get ':id/export/download' do
+ if user_group.export_file_exists?
+ present_carrierwave_file!(user_group.export_file)
+ else
+ render_api_error!('404 Not found or has expired', 404)
+ end
+ end
+
+ desc 'Start export' do
+ detail 'This feature was introduced in GitLab 12.5.'
+ end
+ post ':id/export' do
+ GroupExportWorker.perform_async(current_user.id, user_group.id, params)
+
+ accepted!
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 19c29847ce3..49b86489a8b 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -9,6 +9,7 @@ module API
GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret"
SUDO_PARAM = :sudo
API_USER_ENV = 'gitlab.api.user'
+ API_EXCEPTION_ENV = 'gitlab.api.exception'
def declared_params(options = {})
options = { include_parent_namespaces: false }.merge(options)
@@ -387,6 +388,9 @@ module API
Gitlab::Sentry.track_acceptable_exception(exception, extra: params)
end
+ # This is used with GrapeLogging::Loggers::ExceptionLogger
+ env[API_EXCEPTION_ENV] = exception
+
# lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
trace = exception.backtrace
@@ -451,6 +455,17 @@ module API
end
end
+ def track_event(action = action_name, **args)
+ category = args.delete(:category) || self.options[:for].name
+ raise "invalid category" unless category
+
+ ::Gitlab::Tracking.event(category, action.to_s, **args)
+ rescue => error
+ Rails.logger.warn( # rubocop:disable Gitlab/RailsLogger
+ "Tracking event failed for action: #{action}, category: #{category}, message: #{error.message}"
+ )
+ end
+
protected
def project_finder_params_ce
@@ -464,6 +479,8 @@ module API
finder_params[:user] = params.delete(:user) if params[:user]
finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level]
+ finder_params[:id_after] = params[:id_after] if params[:id_after]
+ finder_params[:id_before] = params[:id_before] if params[:id_before]
finder_params
end
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 4c575381d30..dfac777e4a1 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -140,7 +140,8 @@ module API
{
repository: repository.gitaly_repository,
address: Gitlab::GitalyClient.address(project.repository_storage),
- token: Gitlab::GitalyClient.token(project.repository_storage)
+ token: Gitlab::GitalyClient.token(project.repository_storage),
+ features: Feature::Gitaly.server_feature_flags
}
end
end
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
index 71bbc218f94..9c5b355e823 100644
--- a/lib/api/helpers/pagination.rb
+++ b/lib/api/helpers/pagination.rb
@@ -4,254 +4,7 @@ module API
module Helpers
module Pagination
def paginate(relation)
- strategy = if params[:pagination] == 'keyset' && Feature.enabled?('api_keyset_pagination')
- KeysetPaginationStrategy
- else
- DefaultPaginationStrategy
- end
-
- strategy.new(self).paginate(relation)
- end
-
- class Base
- private
-
- def per_page
- @per_page ||= params[:per_page]
- end
-
- def base_request_uri
- @base_request_uri ||= URI.parse(request.url).tap do |uri|
- uri.host = Gitlab.config.gitlab.host
- uri.port = Gitlab.config.gitlab.port
- end
- end
-
- def build_page_url(query_params:)
- base_request_uri.tap do |uri|
- uri.query = query_params
- end.to_s
- end
-
- def page_href(next_page_params = {})
- query_params = params.merge(**next_page_params, per_page: per_page).to_query
-
- build_page_url(query_params: query_params)
- end
- end
-
- class KeysetPaginationInfo
- attr_reader :relation, :request_context
-
- def initialize(relation, request_context)
- # This is because it's rather complex to support multiple values with possibly different sort directions
- # (and we don't need this in the API)
- if relation.order_values.size > 1
- raise "Pagination only supports ordering by a single column." \
- "The following columns were given: #{relation.order_values.map { |v| v.expr.name }}"
- end
-
- @relation = relation
- @request_context = request_context
- end
-
- def fields
- keys.zip(values).reject { |_, v| v.nil? }.to_h
- end
-
- def column_for_order_by(relation)
- relation.order_values.first&.expr&.name
- end
-
- # Sort direction (`:asc` or `:desc`)
- def sort
- @sort ||= if order_by_primary_key?
- # Default order is by id DESC
- :desc
- else
- # API defaults to DESC order if param `sort` not present
- request_context.params[:sort]&.to_sym || :desc
- end
- end
-
- # Do we only sort by primary key?
- def order_by_primary_key?
- keys.size == 1 && keys.first == primary_key
- end
-
- def primary_key
- relation.model.primary_key.to_sym
- end
-
- def sort_ascending?
- sort == :asc
- end
-
- # Build hash of request parameters for a given record (relevant to pagination)
- def params_for(record)
- return {} unless record
-
- keys.each_with_object({}) do |key, h|
- h["ks_prev_#{key}".to_sym] = record.attributes[key.to_s]
- end
- end
-
- private
-
- # All values present in request parameters that correspond to #keys.
- def values
- @values ||= keys.map do |key|
- request_context.params["ks_prev_#{key}".to_sym]
- end
- end
-
- # All keys relevant to pagination.
- # This always includes the primary key. Optionally, the `order_by` key is prepended.
- def keys
- @keys ||= [column_for_order_by(relation), primary_key].compact.uniq
- end
- end
-
- class KeysetPaginationStrategy < Base
- attr_reader :request_context
- delegate :params, :header, :request, to: :request_context
-
- def initialize(request_context)
- @request_context = request_context
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def paginate(relation)
- pagination = KeysetPaginationInfo.new(relation, request_context)
-
- paged_relation = relation.limit(per_page)
-
- if conds = conditions(pagination)
- paged_relation = paged_relation.where(*conds)
- end
-
- # In all cases: sort by primary key (possibly in addition to another sort column)
- paged_relation = paged_relation.order(pagination.primary_key => pagination.sort)
-
- add_default_pagination_headers
-
- if last_record = paged_relation.last
- next_page_params = pagination.params_for(last_record)
- add_navigation_links(next_page_params)
- end
-
- paged_relation
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def conditions(pagination)
- fields = pagination.fields
-
- return if fields.empty?
-
- placeholder = fields.map { '?' }
-
- comp = if pagination.sort_ascending?
- '>'
- else
- '<'
- end
-
- [
- # Row value comparison:
- # (A, B) < (a, b) <=> (A < a) OR (A = a AND B < b)
- # <=> A <= a AND ((A < a) OR (A = a AND B < b))
- "(#{fields.keys.join(',')}) #{comp} (#{placeholder.join(',')})",
- *fields.values
- ]
- end
-
- def add_default_pagination_headers
- header 'X-Per-Page', per_page.to_s
- end
-
- def add_navigation_links(next_page_params)
- header 'X-Next-Page', page_href(next_page_params)
- header 'Link', link_for('next', next_page_params)
- end
-
- def link_for(rel, next_page_params)
- %(<#{page_href(next_page_params)}>; rel="#{rel}")
- end
- end
-
- class DefaultPaginationStrategy < Base
- attr_reader :request_context
- delegate :params, :header, :request, to: :request_context
-
- def initialize(request_context)
- @request_context = request_context
- end
-
- def paginate(relation)
- paginate_with_limit_optimization(add_default_order(relation)).tap do |data|
- add_pagination_headers(data)
- end
- end
-
- private
-
- def paginate_with_limit_optimization(relation)
- pagination_data = relation.page(params[:page]).per(params[:per_page])
- return pagination_data unless pagination_data.is_a?(ActiveRecord::Relation)
- return pagination_data unless Feature.enabled?(:api_kaminari_count_with_limit)
-
- limited_total_count = pagination_data.total_count_with_limit
- if limited_total_count > Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT
- # The call to `total_count_with_limit` memoizes `@arel` because of a call to `references_eager_loaded_tables?`
- # We need to call `reset` because `without_count` relies on `@arel` being unmemoized
- pagination_data.reset.without_count
- else
- pagination_data
- end
- end
-
- def add_default_order(relation)
- if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty?
- relation = relation.order(:id) # rubocop: disable CodeReuse/ActiveRecord
- end
-
- relation
- end
-
- def add_pagination_headers(paginated_data)
- header 'X-Per-Page', paginated_data.limit_value.to_s
- header 'X-Page', paginated_data.current_page.to_s
- header 'X-Next-Page', paginated_data.next_page.to_s
- header 'X-Prev-Page', paginated_data.prev_page.to_s
- header 'Link', pagination_links(paginated_data)
-
- return if data_without_counts?(paginated_data)
-
- header 'X-Total', paginated_data.total_count.to_s
- header 'X-Total-Pages', total_pages(paginated_data).to_s
- end
-
- def pagination_links(paginated_data)
- [].tap do |links|
- links << %(<#{page_href(page: paginated_data.prev_page)}>; rel="prev") if paginated_data.prev_page
- links << %(<#{page_href(page: paginated_data.next_page)}>; rel="next") if paginated_data.next_page
- links << %(<#{page_href(page: 1)}>; rel="first")
-
- links << %(<#{page_href(page: total_pages(paginated_data))}>; rel="last") unless data_without_counts?(paginated_data)
- end.join(', ')
- end
-
- def total_pages(paginated_data)
- # Ensure there is in total at least 1 page
- [paginated_data.total_pages, 1].max
- end
-
- def data_without_counts?(paginated_data)
- paginated_data.is_a?(Kaminari::PaginatableWithoutCount)
- end
+ ::Gitlab::Pagination::OffsetPagination.new(self).paginate(relation)
end
end
end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 94619204274..47b1f037eb8 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -30,6 +30,7 @@ module API
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
+ optional :remove_source_branch_after_merge, type: Boolean, desc: 'Remove the source branch by default after merge'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.'
@@ -94,6 +95,7 @@ module API
:path,
:printing_merge_request_link_enabled,
:public_builds,
+ :remove_source_branch_after_merge,
:repository_access_level,
:request_access_enabled,
:resolve_outdated_diff_discussions,
@@ -109,7 +111,6 @@ module API
:jobs_enabled,
:merge_requests_enabled,
:wiki_enabled,
- :jobs_enabled,
:snippets_enabled
]
end
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index d9a22484c1f..c70f2f3e2c8 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -77,7 +77,7 @@ module API
response_with_status(**payload)
when ::Gitlab::GitAccessResult::CustomAction
- response_with_status(code: 300, message: check_result.message, payload: check_result.payload)
+ response_with_status(code: 300, payload: check_result.payload, gl_console_messages: check_result.console_messages)
else
response_with_status(code: 500, success: false, message: UNKNOWN_CHECK_RESULT_ERROR)
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 1436238c5cf..6e10414def4 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -296,9 +296,12 @@ module API
end
get ':id/merge_requests/:merge_request_iid/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
- commits = ::Kaminari.paginate_array(merge_request.commits)
- present paginate(commits), with: Entities::Commit
+ commits =
+ paginate(merge_request.merge_request_diff.merge_request_diff_commits)
+ .map { |commit| Commit.from_hash(commit.to_hash, merge_request.project) }
+
+ present commits, with: Entities::Commit
end
desc 'Show the merge request changes' do
@@ -404,7 +407,8 @@ module API
merge_params = HashWithIndifferentAccess.new(
commit_message: params[:merge_commit_message],
squash_commit_message: params[:squash_commit_message],
- should_remove_source_branch: params[:should_remove_source_branch]
+ should_remove_source_branch: params[:should_remove_source_branch],
+ sha: params[:sha] || merge_request.diff_head_sha
)
if merge_when_pipeline_succeeds && merge_request.head_pipeline && merge_request.head_pipeline.active?
@@ -455,6 +459,8 @@ module API
status :accepted
present rebase_in_progress: merge_request.rebase_in_progress?
+ rescue ::MergeRequest::RebaseLockTimeout => e
+ render_api_error!(e.message, 409)
end
desc 'List issues that will be closed on merge' do
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index ec2fe8270b7..2d02a4e624c 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -92,8 +92,10 @@ module API
requires :domain, type: String, desc: 'The domain'
# rubocop:disable Scalability/FileUploads
# TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960
- optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
- optional :key, allow_blank: false, types: [File, String], desc: 'The key', as: :user_provided_key
+ optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
+ optional :key, types: [File, String], desc: 'The key', as: :user_provided_key
+ optional :auto_ssl_enabled, allow_blank: false, type: Boolean, default: false,
+ desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
# rubocop:enable Scalability/FileUploads
all_or_none_of :user_provided_certificate, :user_provided_key
end
@@ -116,14 +118,16 @@ module API
requires :domain, type: String, desc: 'The domain'
# rubocop:disable Scalability/FileUploads
# TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960
- optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
- optional :key, allow_blank: false, types: [File, String], desc: 'The key', as: :user_provided_key
+ optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
+ optional :key, types: [File, String], desc: 'The key', as: :user_provided_key
+ optional :auto_ssl_enabled, allow_blank: true, type: Boolean,
+ desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
# rubocop:enable Scalability/FileUploads
end
put ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do
authorize! :update_pages, user_project
- pages_domain_params = declared(params, include_parent_namespaces: false)
+ pages_domain_params = declared(params, include_parent_namespaces: false, include_missing: false)
# Remove empty private key if certificate is not empty.
if pages_domain_params[:user_provided_certificate] && !pages_domain_params[:user_provided_key]
diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb
index 45c800d7d1e..8e35914f48a 100644
--- a/lib/api/project_clusters.rb
+++ b/lib/api/project_clusters.rb
@@ -88,6 +88,7 @@ module API
requires :cluster_id, type: Integer, desc: 'The cluster ID'
optional :name, type: String, desc: 'Cluster name'
optional :domain, type: String, desc: 'Cluster base domain'
+ optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
optional :token, type: String, desc: 'Token to authenticate against Kubernetes'
diff --git a/lib/api/project_container_repositories.rb b/lib/api/project_container_repositories.rb
index 2a05974509a..2b33069e324 100644
--- a/lib/api/project_container_repositories.rb
+++ b/lib/api/project_container_repositories.rb
@@ -24,9 +24,11 @@ module API
end
get ':id/registry/repositories' do
repositories = ContainerRepositoriesFinder.new(
- id: user_project.id, container_type: :project
+ user: current_user, subject: user_project
).execute
+ track_event( 'list_repositories')
+
present paginate(repositories), with: Entities::ContainerRegistry::Repository, tags: params[:tags]
end
@@ -40,6 +42,7 @@ module API
authorize_admin_container_image!
DeleteContainerRepositoryWorker.perform_async(current_user.id, repository.id)
+ track_event('delete_repository')
status :accepted
end
@@ -56,6 +59,8 @@ module API
authorize_read_container_image!
tags = Kaminari.paginate_array(repository.tags)
+ track_event('list_tags')
+
present paginate(tags), with: Entities::ContainerRegistry::Tag
end
@@ -77,6 +82,8 @@ module API
CleanupContainerRepositoryWorker.perform_async(current_user.id, repository.id,
declared_params.except(:repository_id))
+ track_event('delete_tag_bulk')
+
status :accepted
end
@@ -111,6 +118,8 @@ module API
.execute(repository)
if result[:status] == :success
+ track_event('delete_tag')
+
status :ok
else
status :bad_request
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index d2dacafe7f9..669def2b63c 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -61,6 +61,8 @@ module API
optional :with_merge_requests_enabled, type: Boolean, default: false, desc: 'Limit by enabled merge requests feature'
optional :with_programming_language, type: String, desc: 'Limit to repositories which use the given programming language'
optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user'
+ optional :id_after, type: Integer, desc: 'Limit results to projects with IDs greater than the specified ID'
+ optional :id_before, type: Integer, desc: 'Limit results to projects with IDs less than the specified ID'
use :optional_filter_params_ee
end
@@ -69,7 +71,8 @@ module API
optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
optional :import_url, type: String, desc: 'URL from which the project is imported'
optional :template_name, type: String, desc: "Name of template from which to create project"
- mutually_exclusive :import_url, :template_name
+ optional :template_project_id, type: Integer, desc: "Project ID of template from which to create project"
+ mutually_exclusive :import_url, :template_name, :template_project_id
end
def load_projects
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index 4238529142c..3f600ef4a04 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -45,7 +45,7 @@ module API
end
params do
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
- requires :name, type: String, desc: 'The name of the release'
+ optional :name, type: String, desc: 'The name of the release'
requires :description, type: String, desc: 'The release notes'
optional :ref, type: String, desc: 'The commit sha or branch name'
optional :assets, type: Hash do
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index c90ba0c9b5d..5362b3060c1 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -42,6 +42,7 @@ module API
optional :asset_proxy_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted.'
optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts"
+ optional :default_ci_config_path, type: String, desc: 'The instance default CI configuration path for new projects'
optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group'
optional :default_branch_protection, type: Integer, values: ::Gitlab::Access.protection_values, desc: 'Determine if developers can push to master'
optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility'
@@ -52,6 +53,12 @@ module API
optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
optional :domain_blacklist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
optional :domain_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ optional :eks_integration_enabled, type: Boolean, desc: 'Enable integration with Amazon EKS'
+ given eks_integration_enabled: -> (val) { val } do
+ requires :eks_account_id, type: String, desc: 'Amazon account ID for EKS integration'
+ requires :eks_access_key_id, type: String, desc: 'Access key ID for the EKS integration IAM user'
+ requires :eks_secret_access_key, type: String, desc: 'Secret access key for the EKS integration IAM user'
+ end
optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
@@ -129,16 +136,22 @@ module API
optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application'
optional :signin_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' # support legacy names, can be removed in v5
optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled'
+ optional :sourcegraph_enabled, type: Boolean, desc: 'Enable Sourcegraph'
+ optional :sourcegraph_public_only, type: Boolean, desc: 'Only allow public projects to communicate with Sourcegraph'
+ given sourcegraph_enabled: ->(val) { val } do
+ requires :sourcegraph_url, type: String, desc: 'The configured Sourcegraph instance URL'
+ end
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins'
optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated'
optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5
optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking'
+ optional :snowplow_iglu_registry_url, type: String, desc: 'The Snowplow base Iglu Schema Registry URL to use for custom context and self describing events'
given snowplow_enabled: ->(val) { val } do
requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname'
optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain'
- optional :snowplow_site_id, type: String, desc: 'The Snowplow site name / application ic'
+ optional :snowplow_app_id, type: String, desc: 'The Snowplow site name / application id'
end
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb
index daa9598a204..693c20cb73a 100644
--- a/lib/api/sidekiq_metrics.rb
+++ b/lib/api/sidekiq_metrics.rb
@@ -36,7 +36,8 @@ module API
{
processed: stats.processed,
failed: stats.failed,
- enqueued: stats.enqueued
+ enqueued: stats.enqueued,
+ dead: stats.dead_size
}
end
end
diff --git a/lib/banzai/filter/inline_grafana_metrics_filter.rb b/lib/banzai/filter/inline_grafana_metrics_filter.rb
new file mode 100644
index 00000000000..321580b532f
--- /dev/null
+++ b/lib/banzai/filter/inline_grafana_metrics_filter.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ # HTML filter that inserts a placeholder element for each
+ # reference to a grafana dashboard.
+ class InlineGrafanaMetricsFilter < Banzai::Filter::InlineEmbedsFilter
+ # Placeholder element for the frontend to use as an
+ # injection point for charts.
+ def create_element(params)
+ begin_loading_dashboard(params[:url])
+
+ doc.document.create_element(
+ 'div',
+ class: 'js-render-metrics',
+ 'data-dashboard-url': metrics_dashboard_url(params)
+ )
+ end
+
+ def embed_params(node)
+ query_params = Gitlab::Metrics::Dashboard::Url.parse_query(node['href'])
+ return unless [:panelId, :from, :to].all? do |param|
+ query_params.include?(param)
+ end
+
+ { url: node['href'], start: query_params[:from], end: query_params[:to] }
+ end
+
+ # Selects any links with an href contains the configured
+ # grafana domain for the project
+ def xpath_search
+ return unless grafana_url.present?
+
+ %(descendant-or-self::a[starts-with(@href, '#{grafana_url}')])
+ end
+
+ private
+
+ def project
+ context[:project]
+ end
+
+ def grafana_url
+ project&.grafana_integration&.grafana_url
+ end
+
+ def metrics_dashboard_url(params)
+ Gitlab::Routing.url_helpers.project_grafana_api_metrics_dashboard_url(
+ project,
+ embedded: true,
+ grafana_url: params[:url],
+ start: format_time(params[:start]),
+ end: format_time(params[:end])
+ )
+ end
+
+ # Formats a timestamp from Grafana for compatibility with
+ # parsing in JS via `new Date(timestamp)`
+ #
+ # @param time [String] Represents miliseconds since epoch
+ def format_time(time)
+ Time.at(time.to_i / 1000).utc.strftime('%FT%TZ')
+ end
+
+ # Fetches a dashboard and caches the result for the
+ # FE to fetch quickly while rendering charts
+ def begin_loading_dashboard(url)
+ ::Gitlab::Metrics::Dashboard::Finder.find(
+ project,
+ embedded: true,
+ grafana_url: url
+ )
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb
index 4d8a5028898..e84ba83e03e 100644
--- a/lib/banzai/filter/inline_metrics_redactor_filter.rb
+++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb
@@ -8,14 +8,17 @@ module Banzai
include Gitlab::Utils::StrongMemoize
METRICS_CSS_CLASS = '.js-render-metrics'
+ URL = Gitlab::Metrics::Dashboard::Url
+
+ Embed = Struct.new(:project_path, :permission)
# Finds all embeds based on the css class the FE
# uses to identify the embedded content, removing
# only unnecessary nodes.
def call
nodes.each do |node|
- path = paths_by_node[node]
- user_has_access = user_access_by_path[path]
+ embed = embeds_by_node[node]
+ user_has_access = user_access_by_embed[embed]
node.remove unless user_has_access
end
@@ -30,40 +33,69 @@ module Banzai
end
# Returns all nodes which the FE will identify as
- # a metrics dashboard placeholder element
+ # a metrics embed placeholder element
#
# @return [Nokogiri::XML::NodeSet]
def nodes
@nodes ||= doc.css(METRICS_CSS_CLASS)
end
- # Maps a node to the full path of a project.
+ # Maps a node to key properties of an embed.
# Memoized so we only need to run the regex to get
# the project full path from the url once per node.
#
- # @return [Hash<Nokogiri::XML::Node, String>]
- def paths_by_node
- strong_memoize(:paths_by_node) do
- nodes.each_with_object({}) do |node, paths|
- paths[node] = path_for_node(node)
+ # @return [Hash<Nokogiri::XML::Node, Embed>]
+ def embeds_by_node
+ strong_memoize(:embeds_by_node) do
+ nodes.each_with_object({}) do |node, embeds|
+ embed = Embed.new
+ url = node.attribute('data-dashboard-url').to_s
+
+ set_path_and_permission(embed, url, URL.regex, :read_environment)
+ set_path_and_permission(embed, url, URL.grafana_regex, :read_project) unless embed.permission
+
+ embeds[node] = embed if embed.permission
end
end
end
- # Gets a project's full_path from the dashboard url
- # in the placeholder node. The FE will use the attr
- # `data-dashboard-url`, so we want to check against that
- # attribute directly in case a user has manually
- # created a metrics element (rather than supporting
- # an alternate attr in InlineMetricsFilter).
+ # Attempts to determine the path and permission attributes
+ # of a url based on expected dashboard url formats and
+ # sets the attributes on an Embed object
#
- # @return [String]
- def path_for_node(node)
- url = node.attribute('data-dashboard-url').to_s
-
- Gitlab::Metrics::Dashboard::Url.regex.match(url) do |m|
+ # @param embed [Embed]
+ # @param url [String]
+ # @param regex [RegExp]
+ # @param permission [Symbol]
+ def set_path_and_permission(embed, url, regex, permission)
+ return unless path = regex.match(url) do |m|
"#{$~[:namespace]}/#{$~[:project]}"
end
+
+ embed.project_path = path
+ embed.permission = permission
+ end
+
+ # Returns a mapping representing whether the current user
+ # has permission to view the embed for the project.
+ # Determined in a batch
+ #
+ # @return [Hash<Embed, Boolean>]
+ def user_access_by_embed
+ strong_memoize(:user_access_by_embed) do
+ unique_embeds.each_with_object({}) do |embed, access|
+ project = projects_by_path[embed.project_path]
+
+ access[embed] = Ability.allowed?(user, embed.permission, project)
+ end
+ end
+ end
+
+ # Returns a unique list of embeds
+ #
+ # @return [Array<Embed>]
+ def unique_embeds
+ embeds_by_node.values.uniq
end
# Maps a project's full path to a Project object.
@@ -74,22 +106,17 @@ module Banzai
def projects_by_path
strong_memoize(:projects_by_path) do
Project.eager_load(:route, namespace: [:route])
- .where_full_path_in(paths_by_node.values.uniq)
+ .where_full_path_in(unique_project_paths)
.index_by(&:full_path)
end
end
- # Returns a mapping representing whether the current user
- # has permission to view the metrics for the project.
- # Determined in a batch
+ # Returns a list of the full_paths of every project which
+ # has an embed in the doc
#
- # @return [Hash<Project, Boolean>]
- def user_access_by_path
- strong_memoize(:user_access_by_path) do
- projects_by_path.each_with_object({}) do |(path, project), access|
- access[path] = Ability.allowed?(user, :read_environment, project)
- end
- end
+ # @return [Array<String>]
+ def unique_project_paths
+ embeds_by_node.values.map(&:project_path).uniq
end
end
end
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
index ed82fbc1f94..98987ee2019 100644
--- a/lib/banzai/filter/video_link_filter.rb
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -15,7 +15,7 @@ module Banzai
end
def extra_element_attrs
- { width: "100%" }
+ { width: "400" }
end
end
end
diff --git a/lib/banzai/pipeline/ascii_doc_pipeline.rb b/lib/banzai/pipeline/ascii_doc_pipeline.rb
index 82b99d3de4a..90edc7010f4 100644
--- a/lib/banzai/pipeline/ascii_doc_pipeline.rb
+++ b/lib/banzai/pipeline/ascii_doc_pipeline.rb
@@ -10,6 +10,9 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::ExternalLinkFilter,
Filter::PlantumlFilter,
+ Filter::ColorFilter,
+ Filter::ImageLazyLoadFilter,
+ Filter::ImageLinkFilter,
Filter::AsciiDocPostProcessingFilter
]
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 08e27257fdf..f6c12cdb53b 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -30,6 +30,7 @@ module Banzai
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
Filter::InlineMetricsFilter,
+ Filter::InlineGrafanaMetricsFilter,
Filter::TableOfContentsFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter,
diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb
index a498c9bc213..8d9de2dbc7d 100644
--- a/lib/bitbucket/representation/pull_request.rb
+++ b/lib/bitbucket/representation/pull_request.rb
@@ -16,9 +16,10 @@ module Bitbucket
end
def state
- if raw['state'] == 'MERGED'
+ case raw['state']
+ when 'MERGED'
'merged'
- elsif raw['state'] == 'DECLINED'
+ when 'DECLINED', 'SUPERSEDED'
'closed'
else
'opened'
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 92861c567a8..bc0347f6ea1 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -51,7 +51,7 @@ module ContainerRegistry
def upload_blob(name, content, digest)
upload = faraday.post("/v2/#{name}/blobs/uploads/")
- return unless upload.success?
+ return upload unless upload.success?
location = URI(upload.headers['location'])
diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb
index d99a209dc87..9e9df88373a 100644
--- a/lib/declarative_policy.rb
+++ b/lib/declarative_policy.rb
@@ -74,7 +74,14 @@ module DeclarativePolicy
next unless klass.name
begin
- policy_class = "#{klass.name}Policy".constantize
+ klass_name =
+ if subject_class.respond_to?(:declarative_policy_class)
+ subject_class.declarative_policy_class
+ else
+ "#{klass.name}Policy"
+ end
+
+ policy_class = klass_name.constantize
# NOTE: the < operator here tests whether policy_class
# inherits from Base. We can't use #is_a? because that
diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb
index 81f8ba5c8c3..0ac2d017e1a 100644
--- a/lib/feature/gitaly.rb
+++ b/lib/feature/gitaly.rb
@@ -7,7 +7,6 @@ class Feature
# Server feature flags should use '_' to separate words.
SERVER_FEATURE_FLAGS =
%w[
- cache_invalidator
inforef_uploadpack_cache
get_all_lfs_pointers_go
].freeze
@@ -20,7 +19,7 @@ class Feature
default_on = DEFAULT_ON_FLAGS.include?(feature_flag)
Feature.enabled?("gitaly_#{feature_flag}", default_enabled: default_on)
- rescue ActiveRecord::NoDatabaseError
+ rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
false
end
diff --git a/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
index 15cdd25e711..568104cb30b 100644
--- a/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
+++ b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
@@ -5,7 +5,7 @@ require 'rails/generators'
module Rails
class PostDeploymentMigrationGenerator < Rails::Generators::NamedBase
def create_migration_file
- timestamp = Time.now.strftime('%Y%m%d%H%M%S')
+ timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
template "migration.rb", "db/post_migrate/#{timestamp}_#{file_name}.rb"
end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index ad8e693ccbc..0e6db54eb46 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -47,6 +47,18 @@ module Gitlab
Gitlab.config.gitlab.url == COM_URL || gl_subdomain?
end
+ def self.canary?
+ Gitlab::Utils.to_boolean(ENV['CANARY'])
+ end
+
+ def self.com_and_canary?
+ com? && canary?
+ end
+
+ def self.com_but_not_canary?
+ com? && !canary?
+ end
+
def self.org?
Gitlab.config.gitlab.url == 'https://dev.gitlab.org'
end
diff --git a/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb b/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb
index 33cbe1a62ef..9ea20a4d6a4 100644
--- a/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb
+++ b/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb
@@ -68,3 +68,5 @@ module Gitlab
end
end
end
+
+Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder.prepend_if_ee('EE::Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder')
diff --git a/lib/gitlab/analytics/cycle_analytics/data_collector.rb b/lib/gitlab/analytics/cycle_analytics/data_collector.rb
index 0c0f737f2c9..05b16672912 100644
--- a/lib/gitlab/analytics/cycle_analytics/data_collector.rb
+++ b/lib/gitlab/analytics/cycle_analytics/data_collector.rb
@@ -12,6 +12,8 @@ module Gitlab
class DataCollector
include Gitlab::Utils::StrongMemoize
+ delegate :serialized_records, to: :records_fetcher
+
def initialize(stage:, params: {})
@stage = stage
@params = params
diff --git a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
index 90d03142b2a..2662aa38d6b 100644
--- a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
+++ b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
@@ -130,3 +130,5 @@ module Gitlab
end
end
end
+
+Gitlab::Analytics::CycleAnalytics::RecordsFetcher.prepend_if_ee('EE::Gitlab::Analytics::CycleAnalytics::RecordsFetcher')
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events.rb b/lib/gitlab/analytics/cycle_analytics/stage_events.rb
index 58572446de6..f6e22044142 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events.rb
@@ -47,27 +47,29 @@ module Gitlab
]
}.freeze
- def [](identifier)
+ def self.[](identifier)
events.find { |e| e.identifier.to_s.eql?(identifier.to_s) } || raise(KeyError)
end
# hash for defining ActiveRecord enum: identifier => number
- def to_enum
- ENUM_MAPPING.each_with_object({}) { |(k, v), hash| hash[k.identifier] = v }
+ def self.to_enum
+ enum_mapping.each_with_object({}) { |(k, v), hash| hash[k.identifier] = v }
end
- # will be overridden in EE with custom events
- def pairing_rules
+ def self.pairing_rules
PAIRING_RULES
end
- # will be overridden in EE with custom events
- def events
+ def self.events
EVENTS
end
- module_function :[], :to_enum, :pairing_rules, :events
+ def self.enum_mapping
+ ENUM_MAPPING
+ end
end
end
end
end
+
+Gitlab::Analytics::CycleAnalytics::StageEvents.prepend_if_ee('::EE::Gitlab::Analytics::CycleAnalytics::StageEvents')
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb
index 6af1b90bccc..9f0ca80ba50 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class CodeStageStart < SimpleStageEvent
+ class CodeStageStart < StageEvent
def self.name
s_("CycleAnalyticsEvent|Issue first mentioned in a commit")
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb
index 8c9a80740a9..a159580b7bd 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class IssueCreated < SimpleStageEvent
+ class IssueCreated < StageEvent
def self.name
s_("CycleAnalyticsEvent|Issue created")
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb
index fe7f2d85f8b..a3b7fa16daf 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class IssueFirstMentionedInCommit < SimpleStageEvent
+ class IssueFirstMentionedInCommit < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Issue first mentioned in a commit")
end
@@ -20,12 +20,6 @@ module Gitlab
def timestamp_projection
issue_metrics_table[:first_mentioned_in_commit_at]
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def apply_query_customization(query)
- query.joins(:metrics)
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb
index 77e4092b9ab..0ea98e82ecc 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class IssueStageEnd < SimpleStageEvent
+ class IssueStageEnd < MetricsBasedStageEvent
def self.name
PlanStageStart.name
end
@@ -26,7 +26,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def apply_query_customization(query)
- query.joins(:metrics).where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
+ super.where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb
index 7059c425b8f..013e068e479 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class MergeRequestCreated < SimpleStageEvent
+ class MergeRequestCreated < StageEvent
def self.name
s_("CycleAnalyticsEvent|Merge request created")
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb
index 3d7482eaaf0..654d0befbc3 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class MergeRequestFirstDeployedToProduction < SimpleStageEvent
+ class MergeRequestFirstDeployedToProduction < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Merge request first deployed to production")
end
@@ -23,7 +23,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def apply_query_customization(query)
- query.joins(:metrics).where(timestamp_projection.gteq(mr_table[:created_at]))
+ super.where(timestamp_projection.gteq(mr_table[:created_at]))
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb
index 36bb4d6fc8d..a0b1c12756f 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class MergeRequestLastBuildFinished < SimpleStageEvent
+ class MergeRequestLastBuildFinished < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Merge request last build finish time")
end
@@ -20,12 +20,6 @@ module Gitlab
def timestamp_projection
mr_metrics_table[:latest_build_finished_at]
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def apply_query_customization(query)
- query.joins(:metrics)
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb
index 468d9899cc7..da3b5cdfaa4 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class MergeRequestLastBuildStarted < SimpleStageEvent
+ class MergeRequestLastBuildStarted < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Merge request last build start time")
end
@@ -20,12 +20,6 @@ module Gitlab
def timestamp_projection
mr_metrics_table[:latest_build_started_at]
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def apply_query_customization(query)
- query.joins(:metrics)
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb
index 82ecaf1cd6b..e67a6f7eea6 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class MergeRequestMerged < SimpleStageEvent
+ class MergeRequestMerged < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Merge request merged")
end
@@ -20,12 +20,6 @@ module Gitlab
def timestamp_projection
mr_metrics_table[:merged_at]
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def apply_query_customization(query)
- query.joins(:metrics)
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/metrics_based_stage_event.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/metrics_based_stage_event.rb
new file mode 100644
index 00000000000..4ca8745abe4
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/metrics_based_stage_event.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module StageEvents
+ class MetricsBasedStageEvent < StageEvent
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ query.joins(:metrics)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb
index 7ece7d62faa..37168a1fb0f 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class PlanStageStart < SimpleStageEvent
+ class PlanStageStart < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Issue first associated with a milestone or issue first added to a board")
end
@@ -26,8 +26,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def apply_query_customization(query)
- query
- .joins(:metrics)
+ super
.where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
.where(issue_metrics_table[:first_mentioned_in_commit_at].not_eq(nil))
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
index 607371a32e8..b249f6874e7 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class ProductionStageEnd < SimpleStageEvent
+ class ProductionStageEnd < StageEvent
def self.name
PlanStageStart.name
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/simple_stage_event.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/simple_stage_event.rb
deleted file mode 100644
index 253c489d822..00000000000
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/simple_stage_event.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Analytics
- module CycleAnalytics
- module StageEvents
- # Represents a simple event that usually refers to one database column and does not require additional user input
- class SimpleStageEvent < StageEvent
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb
index aa392140eb5..667d6def414 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb
@@ -35,6 +35,10 @@ module Gitlab
query
end
+ def label_based?
+ false
+ end
+
private
attr_reader :params
diff --git a/lib/gitlab/auth/ip_rate_limiter.rb b/lib/gitlab/auth/ip_rate_limiter.rb
index 74d359bcd28..acb46abb6f3 100644
--- a/lib/gitlab/auth/ip_rate_limiter.rb
+++ b/lib/gitlab/auth/ip_rate_limiter.rb
@@ -21,11 +21,12 @@ module Gitlab
end
def register_fail!
+ return false if trusted_ip?
+
# Allow2Ban.filter will return false if this IP has not failed too often yet
@banned = Rack::Attack::Allow2Ban.filter(ip, config) do
- # If we return false here, the failure for this IP is ignored by Allow2Ban
- # If we return true here, the count for the IP is incremented.
- ip_can_be_banned?
+ # We return true to increment the count for this IP
+ true
end
end
@@ -33,20 +34,16 @@ module Gitlab
@banned
end
+ def trusted_ip?
+ trusted_ips.any? { |netmask| netmask.include?(ip) }
+ end
+
private
def config
Gitlab.config.rack_attack.git_basic_auth
end
- def ip_can_be_banned?
- !trusted_ip?
- end
-
- def trusted_ip?
- trusted_ips.any? { |netmask| netmask.include?(ip) }
- end
-
def trusted_ips
strong_memoize(:trusted_ips) do
config.ip_whitelist.map do |proxy|
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
index eb1d0925c55..4bc0ceedae7 100644
--- a/lib/gitlab/auth/ldap/config.rb
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -21,6 +21,14 @@ module Gitlab
Gitlab.config.ldap.enabled
end
+ def self.sign_in_enabled?
+ enabled? && !prevent_ldap_sign_in?
+ end
+
+ def self.prevent_ldap_sign_in?
+ Gitlab.config.ldap.prevent_ldap_sign_in
+ end
+
def self.servers
Gitlab.config.ldap['servers']&.values || []
end
diff --git a/lib/gitlab/background_migration/legacy_upload_mover.rb b/lib/gitlab/background_migration/legacy_upload_mover.rb
index c9e47f210be..1879a6c5427 100644
--- a/lib/gitlab/background_migration/legacy_upload_mover.rb
+++ b/lib/gitlab/background_migration/legacy_upload_mover.rb
@@ -18,6 +18,7 @@ module Gitlab
def execute
return unless upload
+ return unless upload.model_type == 'Note'
if !project
# if we don't have models associated with the upload we can not move it
diff --git a/lib/gitlab/background_migration/legacy_uploads_migrator.rb b/lib/gitlab/background_migration/legacy_uploads_migrator.rb
index a9d38a27e0c..f7cadb9b00d 100644
--- a/lib/gitlab/background_migration/legacy_uploads_migrator.rb
+++ b/lib/gitlab/background_migration/legacy_uploads_migrator.rb
@@ -14,7 +14,7 @@ module Gitlab
include Database::MigrationHelpers
def perform(start_id, end_id)
- Upload.where(id: start_id..end_id, uploader: 'AttachmentUploader').find_each do |upload|
+ Upload.where(id: start_id..end_id, uploader: 'AttachmentUploader', model_type: 'Note').find_each do |upload|
LegacyUploadMover.new(upload).execute
end
end
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
index f5fb33f1660..23e8be4a9ab 100644
--- a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
+++ b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
@@ -176,7 +176,7 @@ module Gitlab
self.table_name = 'projects'
def self.find_by_full_path(path)
- order_sql = "(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
+ order_sql = Arel.sql("(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)")
where_full_path_in(path).reorder(order_sql).take
end
diff --git a/lib/gitlab/ci/ansi2json/converter.rb b/lib/gitlab/ci/ansi2json/converter.rb
index 8d25b66af9c..cbda3808b86 100644
--- a/lib/gitlab/ci/ansi2json/converter.rb
+++ b/lib/gitlab/ci/ansi2json/converter.rb
@@ -22,11 +22,11 @@ module Gitlab
start_offset = @state.offset
- @state.set_current_line!(style: Style.new(@state.inherited_style))
+ @state.new_line!(
+ style: Style.new(@state.inherited_style))
stream.each_line do |line|
- s = StringScanner.new(line)
- convert_line(s)
+ consume_line(line)
end
# This must be assigned before flushing the current line
@@ -52,26 +52,41 @@ module Gitlab
private
- def convert_line(scanner)
- until scanner.eos?
-
- if scanner.scan(Gitlab::Regex.build_trace_section_regex)
- handle_section(scanner)
- elsif scanner.scan(/\e([@-_])(.*?)([@-~])/)
- handle_sequence(scanner)
- elsif scanner.scan(/\e(([@-_])(.*?)?)?$/)
- break
- elsif scanner.scan(/</)
- @state.current_line << '&lt;'
- elsif scanner.scan(/\r?\n/)
- # we advance the offset of the next current line
- # so it does not start from \n
- flush_current_line(advance_offset: scanner.matched_size)
- else
- @state.current_line << scanner.scan(/./m)
- end
-
- @state.offset += scanner.matched_size
+ def consume_line(line)
+ scanner = StringScanner.new(line)
+
+ consume_token(scanner) until scanner.eos?
+ end
+
+ def consume_token(scanner)
+ if scan_token(scanner, Gitlab::Regex.build_trace_section_regex, consume: false)
+ handle_section(scanner)
+ elsif scan_token(scanner, /\e([@-_])(.*?)([@-~])/)
+ handle_sequence(scanner)
+ elsif scan_token(scanner, /\e(([@-_])(.*?)?)?$/)
+ # stop scanning
+ scanner.terminate
+ elsif scan_token(scanner, /\r?\n/)
+ flush_current_line
+ elsif scan_token(scanner, /\r/)
+ # drop last line
+ @state.current_line.clear!
+ elsif scan_token(scanner, /.[^\e\r\ns]*/m)
+ # this is a join from all previous tokens and first letters
+ # it always matches at least one character `.`
+ # it matches everything that is not start of:
+ # `\e`, `<`, `\r`, `\n`, `s` (for section_start)
+ @state.current_line << scanner[0]
+ else
+ raise 'invalid parser state'
+ end
+ end
+
+ def scan_token(scanner, match, consume: true)
+ scanner.scan(match).tap do |result|
+ # we need to move offset as soon
+ # as we match the token
+ @state.offset += scanner.matched_size if consume && result
end
end
@@ -96,32 +111,50 @@ module Gitlab
section_name = sanitize_section_name(section)
if action == "start"
- handle_section_start(section_name, timestamp)
+ handle_section_start(scanner, section_name, timestamp)
elsif action == "end"
- handle_section_end(section_name, timestamp)
+ handle_section_end(scanner, section_name, timestamp)
+ else
+ raise 'unsupported action'
end
end
- def handle_section_start(section, timestamp)
- flush_current_line unless @state.current_line.empty?
+ def handle_section_start(scanner, section, timestamp)
+ # We make a new line for new section
+ flush_current_line
+
@state.open_section(section, timestamp)
+
+ # we need to consume match after handling
+ # the open of section, as we want the section
+ # marker to be refresh on incremental update
+ @state.offset += scanner.matched_size
end
- def handle_section_end(section, timestamp)
+ def handle_section_end(scanner, section, timestamp)
return unless @state.section_open?(section)
- flush_current_line unless @state.current_line.empty?
+ # We flush the content to make the end
+ # of section to be a new line
+ flush_current_line
+
@state.close_section(section, timestamp)
- # ensure that section end is detached from the last
- # line in the section
+ # we need to consume match before handling
+ # as we want the section close marker
+ # not to be refreshed on incremental update
+ @state.offset += scanner.matched_size
+
+ # this flushes an empty line with `section_duration`
flush_current_line
end
- def flush_current_line(advance_offset: 0)
- @lines << @state.current_line.to_h
+ def flush_current_line
+ unless @state.current_line.empty?
+ @lines << @state.current_line.to_h
+ end
- @state.set_current_line!(advance_offset: advance_offset)
+ @state.new_line!
end
def sanitize_section_name(section)
diff --git a/lib/gitlab/ci/ansi2json/line.rb b/lib/gitlab/ci/ansi2json/line.rb
index 173fb1df88e..21aa1f84353 100644
--- a/lib/gitlab/ci/ansi2json/line.rb
+++ b/lib/gitlab/ci/ansi2json/line.rb
@@ -47,12 +47,17 @@ module Gitlab
@current_segment.text << data
end
+ def clear!
+ @segments.clear
+ @current_segment = Segment.new(style: style)
+ end
+
def style
@current_segment.style
end
def empty?
- @segments.empty? && @current_segment.empty?
+ @segments.empty? && @current_segment.empty? && @section_duration.nil?
end
def update_style(ansi_commands)
diff --git a/lib/gitlab/ci/ansi2json/state.rb b/lib/gitlab/ci/ansi2json/state.rb
index db7a9035b8b..7e1a8102a35 100644
--- a/lib/gitlab/ci/ansi2json/state.rb
+++ b/lib/gitlab/ci/ansi2json/state.rb
@@ -46,9 +46,9 @@ module Gitlab
@open_sections.key?(section)
end
- def set_current_line!(style: nil, advance_offset: 0)
+ def new_line!(style: nil)
new_line = Line.new(
- offset: @offset + advance_offset,
+ offset: @offset,
style: style || @current_line.style,
sections: @open_sections.keys
)
diff --git a/lib/gitlab/ci/ansi2json/style.rb b/lib/gitlab/ci/ansi2json/style.rb
index 2739ffdfa5d..77f61178b37 100644
--- a/lib/gitlab/ci/ansi2json/style.rb
+++ b/lib/gitlab/ci/ansi2json/style.rb
@@ -15,14 +15,10 @@ module Gitlab
end
def update(ansi_commands)
- command = ansi_commands.shift
- return unless command
-
- if changes = Gitlab::Ci::Ansi2json::Parser.new(command, ansi_commands).changes
- apply_changes(changes)
- end
+ # treat e\[m as \e[0m
+ ansi_commands = ['0'] if ansi_commands.empty?
- update(ansi_commands)
+ evaluate_stack_command(ansi_commands)
end
def set?
@@ -50,6 +46,17 @@ module Gitlab
private
+ def evaluate_stack_command(ansi_commands)
+ command = ansi_commands.shift
+ return unless command
+
+ if changes = Gitlab::Ci::Ansi2json::Parser.new(command, ansi_commands).changes
+ apply_changes(changes)
+ end
+
+ evaluate_stack_command(ansi_commands)
+ end
+
def apply_changes(changes)
case
when changes[:reset]
diff --git a/lib/gitlab/ci/build/context/base.rb b/lib/gitlab/ci/build/context/base.rb
new file mode 100644
index 00000000000..02b97ea76e9
--- /dev/null
+++ b/lib/gitlab/ci/build/context/base.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ module Context
+ class Base
+ attr_reader :pipeline
+
+ def initialize(pipeline)
+ @pipeline = pipeline
+ end
+
+ def variables
+ raise NotImplementedError
+ end
+
+ protected
+
+ def pipeline_attributes
+ {
+ pipeline: pipeline,
+ project: pipeline.project,
+ user: pipeline.user,
+ ref: pipeline.ref,
+ tag: pipeline.tag,
+ trigger_request: pipeline.legacy_trigger,
+ protected: pipeline.protected_ref?
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/context/build.rb b/lib/gitlab/ci/build/context/build.rb
new file mode 100644
index 00000000000..dfd86d3ad72
--- /dev/null
+++ b/lib/gitlab/ci/build/context/build.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ module Context
+ class Build < Base
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :attributes
+
+ def initialize(pipeline, attributes = {})
+ super(pipeline)
+
+ @attributes = attributes
+ end
+
+ def variables
+ strong_memoize(:variables) do
+ # This is a temporary piece of technical debt to allow us access
+ # to the CI variables to evaluate rules before we persist a Build
+ # with the result. We should refactor away the extra Build.new,
+ # but be able to get CI Variables directly from the Seed::Build.
+ stub_build.scoped_variables_hash
+ end
+ end
+
+ private
+
+ def stub_build
+ ::Ci::Build.new(build_attributes)
+ end
+
+ def build_attributes
+ attributes.merge(pipeline_attributes)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/context/global.rb b/lib/gitlab/ci/build/context/global.rb
new file mode 100644
index 00000000000..fdd3ac358d5
--- /dev/null
+++ b/lib/gitlab/ci/build/context/global.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ module Context
+ class Global < Base
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(pipeline, yaml_variables:)
+ super(pipeline)
+
+ @yaml_variables = yaml_variables.to_a
+ end
+
+ def variables
+ strong_memoize(:variables) do
+ # This is a temporary piece of technical debt to allow us access
+ # to the CI variables to evaluate workflow:rules
+ # with the result. We should refactor away the extra Build.new,
+ # but be able to get CI Variables directly from the Seed::Build.
+ stub_build.scoped_variables_hash
+ .reject { |key, _value| key =~ /\ACI_(JOB|BUILD)/ }
+ end
+ end
+
+ private
+
+ def stub_build
+ ::Ci::Build.new(build_attributes)
+ end
+
+ def build_attributes
+ pipeline_attributes.merge(
+ yaml_variables: @yaml_variables)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/policy/changes.rb b/lib/gitlab/ci/build/policy/changes.rb
index 9c705a1cd3e..9ae4198bbf7 100644
--- a/lib/gitlab/ci/build/policy/changes.rb
+++ b/lib/gitlab/ci/build/policy/changes.rb
@@ -9,7 +9,7 @@ module Gitlab
@globs = Array(globs)
end
- def satisfied_by?(pipeline, seed)
+ def satisfied_by?(pipeline, context)
return true if pipeline.modified_paths.nil?
pipeline.modified_paths.any? do |path|
diff --git a/lib/gitlab/ci/build/policy/kubernetes.rb b/lib/gitlab/ci/build/policy/kubernetes.rb
index 4c7dc947cd0..4e8693724e5 100644
--- a/lib/gitlab/ci/build/policy/kubernetes.rb
+++ b/lib/gitlab/ci/build/policy/kubernetes.rb
@@ -11,7 +11,7 @@ module Gitlab
end
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
pipeline.has_kubernetes_active?
end
end
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
index c3005303fd8..afe0ccb361e 100644
--- a/lib/gitlab/ci/build/policy/refs.rb
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -9,7 +9,7 @@ module Gitlab
@patterns = Array(refs)
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
@patterns.any? do |pattern|
pattern, path = pattern.split('@', 2)
diff --git a/lib/gitlab/ci/build/policy/specification.rb b/lib/gitlab/ci/build/policy/specification.rb
index ceb5210cfb5..1394340ce1f 100644
--- a/lib/gitlab/ci/build/policy/specification.rb
+++ b/lib/gitlab/ci/build/policy/specification.rb
@@ -17,7 +17,7 @@ module Gitlab
@spec = spec
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
raise NotImplementedError
end
end
diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb
index e9c8864123f..7b1ce6330f0 100644
--- a/lib/gitlab/ci/build/policy/variables.rb
+++ b/lib/gitlab/ci/build/policy/variables.rb
@@ -9,8 +9,8 @@ module Gitlab
@expressions = Array(expressions)
end
- def satisfied_by?(pipeline, seed)
- variables = seed.scoped_variables_hash
+ def satisfied_by?(pipeline, context)
+ variables = context.variables
statements = @expressions.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement
diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
index 43399c74457..c705b6f86c7 100644
--- a/lib/gitlab/ci/build/rules.rb
+++ b/lib/gitlab/ci/build/rules.rb
@@ -13,17 +13,21 @@ module Gitlab
options: { start_in: start_in }.compact
}.compact
end
+
+ def pass?
+ self.when != 'never'
+ end
end
- def initialize(rule_hashes, default_when = 'on_success')
+ def initialize(rule_hashes, default_when:)
@rule_list = Rule.fabricate_list(rule_hashes)
@default_when = default_when
end
- def evaluate(pipeline, build)
+ def evaluate(pipeline, context)
if @rule_list.nil?
Result.new(@default_when)
- elsif matched_rule = match_rule(pipeline, build)
+ elsif matched_rule = match_rule(pipeline, context)
Result.new(
matched_rule.attributes[:when] || @default_when,
matched_rule.attributes[:start_in]
@@ -35,8 +39,8 @@ module Gitlab
private
- def match_rule(pipeline, build)
- @rule_list.find { |rule| rule.matches?(pipeline, build) }
+ def match_rule(pipeline, context)
+ @rule_list.find { |rule| rule.matches?(pipeline, context) }
end
end
end
diff --git a/lib/gitlab/ci/build/rules/rule.rb b/lib/gitlab/ci/build/rules/rule.rb
index 8d52158c8d2..077e4d150fb 100644
--- a/lib/gitlab/ci/build/rules/rule.rb
+++ b/lib/gitlab/ci/build/rules/rule.rb
@@ -23,8 +23,8 @@ module Gitlab
end
end
- def matches?(pipeline, build)
- @clauses.all? { |clause| clause.satisfied_by?(pipeline, build) }
+ def matches?(pipeline, context)
+ @clauses.all? { |clause| clause.satisfied_by?(pipeline, context) }
end
end
end
diff --git a/lib/gitlab/ci/build/rules/rule/clause.rb b/lib/gitlab/ci/build/rules/rule/clause.rb
index bf787fe95a6..6d4bbbb8c21 100644
--- a/lib/gitlab/ci/build/rules/rule/clause.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause.rb
@@ -20,7 +20,7 @@ module Gitlab
@spec = spec
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
raise NotImplementedError
end
end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
index 81d2ee6c24c..728a66ca87f 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
@@ -8,7 +8,7 @@ module Gitlab
@globs = Array(globs)
end
- def satisfied_by?(pipeline, seed)
+ def satisfied_by?(pipeline, context)
return true if pipeline.modified_paths.nil?
pipeline.modified_paths.any? do |path|
diff --git a/lib/gitlab/ci/build/rules/rule/clause/exists.rb b/lib/gitlab/ci/build/rules/rule/clause/exists.rb
index 62f8371283f..85e77438f51 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/exists.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/exists.rb
@@ -15,7 +15,7 @@ module Gitlab
@exact_globs, @pattern_globs = globs.partition(&method(:exact_glob?))
end
- def satisfied_by?(pipeline, seed)
+ def satisfied_by?(pipeline, context)
paths = worktree_paths(pipeline)
exact_matches?(paths) || pattern_matches?(paths)
diff --git a/lib/gitlab/ci/build/rules/rule/clause/if.rb b/lib/gitlab/ci/build/rules/rule/clause/if.rb
index 18c3b450f95..6143a736ca6 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/if.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/if.rb
@@ -8,10 +8,9 @@ module Gitlab
@expression = expression
end
- def satisfied_by?(pipeline, seed)
- variables = seed.scoped_variables_hash
-
- ::Gitlab::Ci::Pipeline::Expression::Statement.new(@expression, variables).truthful?
+ def satisfied_by?(pipeline, context)
+ ::Gitlab::Ci::Pipeline::Expression::Statement.new(
+ @expression, context.variables).truthful?
end
end
end
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index 41613369ca2..9d8d7675234 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -12,7 +12,9 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[name untracked paths reports when expire_in].freeze
+ ALLOWED_KEYS = %i[name untracked paths reports when expire_in expose_as].freeze
+ EXPOSE_AS_REGEX = /\A\w[-\w ]*\z/.freeze
+ EXPOSE_AS_ERROR_MESSAGE = "can contain only letters, digits, '-', '_' and spaces"
attributes ALLOWED_KEYS
@@ -21,11 +23,18 @@ module Gitlab
validations do
validates :config, type: Hash
validates :config, allowed_keys: ALLOWED_KEYS
+ validates :paths, presence: true, if: :expose_as_present?
with_options allow_nil: true do
validates :name, type: String
validates :untracked, boolean: true
validates :paths, array_of_strings: true
+ validates :paths, array_of_strings: {
+ with: /\A[^*]*\z/,
+ message: "can't contain '*' when used with 'expose_as'"
+ }, if: :expose_as_present?
+ validates :expose_as, type: String, length: { maximum: 100 }, if: :expose_as_present?
+ validates :expose_as, format: { with: EXPOSE_AS_REGEX, message: EXPOSE_AS_ERROR_MESSAGE }, if: :expose_as_present?
validates :reports, type: Hash
validates :when,
inclusion: { in: %w[on_success on_failure always],
@@ -41,6 +50,12 @@ module Gitlab
@config[:reports] = reports_value if @config.key?(:reports)
@config
end
+
+ def expose_as_present?
+ return false unless Feature.enabled?(:ci_expose_arbitrary_artifacts_in_mr, default_enabled: true)
+
+ !@config[:expose_as].nil?
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/boolean.rb b/lib/gitlab/ci/config/entry/boolean.rb
new file mode 100644
index 00000000000..10619ef9f8d
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/boolean.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents the interrutible value.
+ #
+ class Boolean < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, boolean: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/commands.rb b/lib/gitlab/ci/config/entry/commands.rb
index 02e368c1813..7a86fca3056 100644
--- a/lib/gitlab/ci/config/entry/commands.rb
+++ b/lib/gitlab/ci/config/entry/commands.rb
@@ -11,11 +11,11 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
validations do
- validates :config, array_of_strings_or_string: true
+ validates :config, string_or_nested_array_of_strings: true
end
def value
- Array(@config)
+ Array(@config).flatten(1)
end
end
end
diff --git a/lib/gitlab/ci/config/entry/default.rb b/lib/gitlab/ci/config/entry/default.rb
index 6200d7c7f87..83127bde6e4 100644
--- a/lib/gitlab/ci/config/entry/default.rb
+++ b/lib/gitlab/ci/config/entry/default.rb
@@ -11,11 +11,10 @@ module Gitlab
#
class Default < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
-
- DuplicateError = Class.new(Gitlab::Config::Loader::FormatError)
+ include ::Gitlab::Config::Entry::Inheritable
ALLOWED_KEYS = %i[before_script image services
- after_script cache].freeze
+ after_script cache interruptible].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -41,31 +40,22 @@ module Gitlab
description: 'Configure caching between build jobs.',
inherit: true
- helpers :before_script, :image, :services, :after_script, :cache
-
- def compose!(deps = nil)
- super(self)
+ entry :interruptible, Entry::Boolean,
+ description: 'Set jobs interruptible default value.',
+ inherit: false
- inherit!(deps)
- end
+ helpers :before_script, :image, :services, :after_script, :cache, :interruptible
private
- def inherit!(deps)
- return unless deps
-
- self.class.nodes.each do |key, factory|
- next unless factory.inheritable?
+ def overwrite_entry(deps, key, current_entry)
+ inherited_entry = deps[key]
- root_entry = deps[key]
- next unless root_entry.specified?
-
- if self[key].specified?
- raise DuplicateError, "#{key} is defined in top-level and `default:` entry"
- end
-
- @entries[key] = root_entry
+ if inherited_entry.specified? && current_entry.specified?
+ raise InheritError, "#{key} is defined in top-level and `default:` entry"
end
+
+ inherited_entry unless current_entry.specified?
end
end
end
diff --git a/lib/gitlab/ci/config/entry/files.rb b/lib/gitlab/ci/config/entry/files.rb
new file mode 100644
index 00000000000..d0d6a36d754
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/files.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents an array of file paths.
+ #
+ class Files < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ validates :config, length: {
+ minimum: 1,
+ maximum: 2,
+ too_short: 'requires at least %{count} item',
+ too_long: 'has too many items (maximum is %{count})'
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 07d5be86b1e..c75ae87a985 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -10,6 +10,7 @@ module Gitlab
class Job < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Inheritable
ALLOWED_WHEN = %w[on_success on_failure always manual delayed].freeze
ALLOWED_KEYS = %i[tags script only except rules type image services
@@ -37,7 +38,6 @@ module Gitlab
with_options allow_nil: true do
validates :tags, array_of_strings: true
validates :allow_failure, boolean: true
- validates :interruptible, boolean: true
validates :parallel, numericality: { only_integer: true,
greater_than_or_equal_to: 2,
less_than_or_equal_to: 50 }
@@ -49,7 +49,6 @@ module Gitlab
validates :timeout, duration: { limit: ChronicDuration.output(Project::MAX_BUILD_TIMEOUT) }
validates :dependencies, array_of_strings: true
- validates :needs, array_of_strings: true
validates :extends, array_of_strings_or_string: true
validates :rules, array_of_hashes: true
end
@@ -73,13 +72,16 @@ module Gitlab
inherit: true
entry :script, Entry::Commands,
- description: 'Commands that will be executed in this job.'
+ description: 'Commands that will be executed in this job.',
+ inherit: false
entry :stage, Entry::Stage,
- description: 'Pipeline stage this job will be executed into.'
+ description: 'Pipeline stage this job will be executed into.',
+ inherit: false
entry :type, Entry::Stage,
- description: 'Deprecated: stage this job will be executed into.'
+ description: 'Deprecated: stage this job will be executed into.',
+ inherit: false
entry :after_script, Entry::Script,
description: 'Commands that will be executed when finishing job.',
@@ -97,30 +99,50 @@ module Gitlab
description: 'Services that will be used to execute this job.',
inherit: true
+ entry :interruptible, Entry::Boolean,
+ description: 'Set jobs interruptible value.',
+ inherit: true
+
entry :only, Entry::Policy,
description: 'Refs policy this job will be executed for.',
- default: Entry::Policy::DEFAULT_ONLY
+ default: Entry::Policy::DEFAULT_ONLY,
+ inherit: false
entry :except, Entry::Policy,
- description: 'Refs policy this job will be executed for.'
+ description: 'Refs policy this job will be executed for.',
+ inherit: false
entry :rules, Entry::Rules,
- description: 'List of evaluable Rules to determine job inclusion.'
+ description: 'List of evaluable Rules to determine job inclusion.',
+ inherit: false,
+ metadata: {
+ allowed_when: %w[on_success on_failure always never manual delayed].freeze
+ }
+
+ entry :needs, Entry::Needs,
+ description: 'Needs configuration for this job.',
+ metadata: { allowed_needs: %i[job] },
+ inherit: false
entry :variables, Entry::Variables,
- description: 'Environment variables available for this job.'
+ description: 'Environment variables available for this job.',
+ inherit: false
entry :artifacts, Entry::Artifacts,
- description: 'Artifacts configuration for this job.'
+ description: 'Artifacts configuration for this job.',
+ inherit: false
entry :environment, Entry::Environment,
- description: 'Environment configuration for this job.'
+ description: 'Environment configuration for this job.',
+ inherit: false
entry :coverage, Entry::Coverage,
- description: 'Coverage configuration for this job.'
+ description: 'Coverage configuration for this job.',
+ inherit: false
entry :retry, Entry::Retry,
- description: 'Retry configuration for this job.'
+ description: 'Retry configuration for this job.',
+ inherit: false
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
@@ -155,8 +177,6 @@ module Gitlab
@entries.delete(:except)
end
end
-
- inherit!(deps)
end
def name
@@ -185,21 +205,8 @@ module Gitlab
private
- # We inherit config entries from `default:`
- # if the entry has the `inherit: true` flag set
- def inherit!(deps)
- return unless deps
-
- self.class.nodes.each do |key, factory|
- next unless factory.inheritable?
-
- default_entry = deps.default[key]
- job_entry = self[key]
-
- if default_entry.specified? && !job_entry.specified?
- @entries[key] = default_entry
- end
- end
+ def overwrite_entry(deps, key, current_entry)
+ deps.default[key] unless current_entry.specified?
end
def to_hash
diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb
index 0c10967e629..f12f0919348 100644
--- a/lib/gitlab/ci/config/entry/key.rb
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -7,11 +7,48 @@ module Gitlab
##
# Entry that represents a key.
#
- class Key < ::Gitlab::Config::Entry::Node
- include ::Gitlab::Config::Entry::Validatable
+ class Key < ::Gitlab::Config::Entry::Simplifiable
+ strategy :SimpleKey, if: -> (config) { config.is_a?(String) || config.is_a?(Symbol) }
+ strategy :ComplexKey, if: -> (config) { config.is_a?(Hash) }
- validations do
- validates :config, key: true
+ class SimpleKey < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, key: true
+ end
+
+ def self.default
+ 'default'
+ end
+
+ def value
+ super.to_s
+ end
+ end
+
+ class ComplexKey < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Configurable
+
+ ALLOWED_KEYS = %i[files prefix].freeze
+ REQUIRED_KEYS = %i[files].freeze
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, required_keys: REQUIRED_KEYS
+ end
+
+ entry :files, Entry::Files,
+ description: 'Files that should be used to build the key'
+ entry :prefix, Entry::Prefix,
+ description: 'Prefix that is added to the final cache key'
+ end
+
+ class UnknownStrategy < ::Gitlab::Config::Entry::Node
+ def errors
+ ["#{location} should be a hash, a string or a symbol"]
+ end
end
def self.default
diff --git a/lib/gitlab/ci/config/entry/need.rb b/lib/gitlab/ci/config/entry/need.rb
new file mode 100644
index 00000000000..b6db546d8ff
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/need.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Need < ::Gitlab::Config::Entry::Simplifiable
+ strategy :Job, if: -> (config) { config.is_a?(String) }
+
+ class Job < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, presence: true
+ validates :config, type: String
+ end
+
+ def type
+ :job
+ end
+
+ def value
+ { name: @config }
+ end
+ end
+
+ class UnknownStrategy < ::Gitlab::Config::Entry::Node
+ def type
+ end
+
+ def value
+ end
+
+ def errors
+ ["#{location} has an unsupported type"]
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+::Gitlab::Ci::Config::Entry::Need.prepend_if_ee('::EE::Gitlab::Ci::Config::Entry::Need')
diff --git a/lib/gitlab/ci/config/entry/needs.rb b/lib/gitlab/ci/config/entry/needs.rb
new file mode 100644
index 00000000000..28452aaaa16
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/needs.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a set of needs dependencies.
+ #
+ class Needs < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, presence: true
+
+ validate do
+ unless config.is_a?(Hash) || config.is_a?(Array)
+ errors.add(:config, 'can only be a Hash or an Array')
+ end
+ end
+
+ validate on: :composed do
+ extra_keys = value.keys - opt(:allowed_needs)
+ if extra_keys.any?
+ errors.add(:config, "uses invalid types: #{extra_keys.join(', ')}")
+ end
+ end
+ end
+
+ def compose!(deps = nil)
+ super(deps) do
+ [@config].flatten.each_with_index do |need, index|
+ @entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Need)
+ .value(need)
+ .with(key: "need", parent: self, description: "need definition.") # rubocop:disable CodeReuse/ActiveRecord
+ .create!
+ end
+
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
+ end
+ end
+
+ def value
+ values = @entries.values.select(&:type)
+ values.group_by(&:type).transform_values do |values|
+ values.map(&:value)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/prefix.rb b/lib/gitlab/ci/config/entry/prefix.rb
new file mode 100644
index 00000000000..3244ad6d611
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/prefix.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a key prefix.
+ #
+ class Prefix < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, key: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/root.rb b/lib/gitlab/ci/config/entry/root.rb
index 07022ff7b54..25fb278d9b8 100644
--- a/lib/gitlab/ci/config/entry/root.rb
+++ b/lib/gitlab/ci/config/entry/root.rb
@@ -12,7 +12,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
ALLOWED_KEYS = %i[default include before_script image services
- after_script variables stages types cache].freeze
+ after_script variables stages types cache workflow].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -64,6 +64,9 @@ module Gitlab
description: 'Configure caching between build jobs.',
reserved: true
+ entry :workflow, Entry::Workflow,
+ description: 'List of evaluable rules to determine Pipeline status'
+
helpers :default, :jobs, :stages, :types, :variables
delegate :before_script_value,
diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb
index 5d6d1c026e3..59e0ef583ae 100644
--- a/lib/gitlab/ci/config/entry/rules/rule.rb
+++ b/lib/gitlab/ci/config/entry/rules/rule.rb
@@ -8,9 +8,9 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- CLAUSES = %i[if changes exists].freeze
- ALLOWED_KEYS = %i[if changes exists when start_in].freeze
- ALLOWED_WHEN = %w[on_success on_failure always never manual delayed].freeze
+ CLAUSES = %i[if changes exists].freeze
+ ALLOWED_KEYS = %i[if changes exists when start_in].freeze
+ ALLOWABLE_WHEN = %w[on_success on_failure always never manual delayed].freeze
attributes :if, :changes, :exists, :when, :start_in
@@ -25,7 +25,14 @@ module Gitlab
with_options allow_nil: true do
validates :if, expression: true
validates :changes, :exists, array_of_strings: true, length: { maximum: 50 }
- validates :when, allowed_values: { in: ALLOWED_WHEN }
+ validates :when, allowed_values: { in: ALLOWABLE_WHEN }
+ end
+
+ validate do
+ validates_with Gitlab::Config::Entry::Validators::AllowedValuesValidator,
+ attributes: %i[when],
+ allow_nil: true,
+ in: opt(:allowed_when)
end
end
diff --git a/lib/gitlab/ci/config/entry/script.rb b/lib/gitlab/ci/config/entry/script.rb
index 9d25a82b521..285e18218b3 100644
--- a/lib/gitlab/ci/config/entry/script.rb
+++ b/lib/gitlab/ci/config/entry/script.rb
@@ -11,7 +11,11 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
validations do
- validates :config, array_of_strings: true
+ validates :config, nested_array_of_strings: true
+ end
+
+ def value
+ config.flatten(1)
end
end
end
diff --git a/lib/gitlab/ci/config/entry/workflow.rb b/lib/gitlab/ci/config/entry/workflow.rb
new file mode 100644
index 00000000000..a51a3fbdcd2
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/workflow.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Workflow < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+
+ ALLOWED_KEYS = %i[rules].freeze
+
+ validations do
+ validates :config, type: Hash
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, presence: true
+ end
+
+ entry :rules, Entry::Rules,
+ description: 'List of evaluable Rules to determine Pipeline status.',
+ metadata: { allowed_when: %w[always never] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb
index 09f9bf5f69f..e714ef225f5 100644
--- a/lib/gitlab/ci/config/normalizer.rb
+++ b/lib/gitlab/ci/config/normalizer.rb
@@ -18,8 +18,8 @@ module Gitlab
config[:dependencies] = expand_names(config[:dependencies])
end
- if config[:needs]
- config[:needs] = expand_names(config[:needs])
+ if job_needs = config.dig(:needs, :job)
+ config[:needs][:job] = expand_needs(job_needs)
end
config
@@ -36,6 +36,22 @@ module Gitlab
end
end
+ def expand_needs(job_needs)
+ return unless job_needs
+
+ job_needs.flat_map do |job_need|
+ job_need_name = job_need[:name].to_sym
+
+ if all_job_names = parallelized_jobs[job_need_name]
+ all_job_names.map do |job_name|
+ { name: job_name }
+ end
+ else
+ job_need
+ end
+ end
+ end
+
def parallelized_jobs
strong_memoize(:parallelized_jobs) do
@jobs_config.each_with_object({}) do |(job_name, config), hash|
diff --git a/lib/gitlab/ci/pipeline/chain/base.rb b/lib/gitlab/ci/pipeline/chain/base.rb
index bab1c73e2f1..aabdf7ce47d 100644
--- a/lib/gitlab/ci/pipeline/chain/base.rb
+++ b/lib/gitlab/ci/pipeline/chain/base.rb
@@ -5,7 +5,7 @@ module Gitlab
module Pipeline
module Chain
class Base
- attr_reader :pipeline, :command
+ attr_reader :pipeline, :command, :config
delegate :project, :current_user, to: :command
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index 899df81ea5c..9662209f88e 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -22,8 +22,6 @@ module Gitlab
external_pull_request: @command.external_pull_request,
variables_attributes: Array(@command.variables_attributes)
)
-
- @pipeline.set_config_source
end
def break?
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index 58f89a6be5e..c2df419cca0 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -10,7 +10,9 @@ module Gitlab
:trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options,
- :chat_data, :allow_mirror_update
+ :chat_data, :allow_mirror_update,
+ # These attributes are set by Chains during processing:
+ :config_content, :config_processor, :stage_seeds
) do
include Gitlab::Utils::StrongMemoize
diff --git a/lib/gitlab/ci/pipeline/chain/config/content.rb b/lib/gitlab/ci/pipeline/chain/config/content.rb
new file mode 100644
index 00000000000..a8cd99b8e92
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/content.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Content < Chain::Base
+ include Chain::Helpers
+
+ def perform!
+ return if @command.config_content
+
+ if content = content_from_repo
+ @command.config_content = content
+ @pipeline.config_source = :repository_source
+ # TODO: we should persist ci_config_path
+ # @pipeline.config_path = ci_config_path
+ elsif content = content_from_auto_devops
+ @command.config_content = content
+ @pipeline.config_source = :auto_devops_source
+ end
+
+ unless @command.config_content
+ return error("Missing #{ci_config_path} file")
+ end
+ end
+
+ def break?
+ @pipeline.errors.any? || @pipeline.persisted?
+ end
+
+ private
+
+ def content_from_repo
+ return unless project
+ return unless @pipeline.sha
+ return unless ci_config_path
+
+ project.repository.gitlab_ci_yml_for(@pipeline.sha, ci_config_path)
+ rescue GRPC::NotFound, GRPC::Internal
+ nil
+ end
+
+ def content_from_auto_devops
+ return unless project&.auto_devops_enabled?
+
+ Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
+ end
+
+ def ci_config_path
+ project.ci_config_path.presence || '.gitlab-ci.yml'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/config/process.rb b/lib/gitlab/ci/pipeline/chain/config/process.rb
new file mode 100644
index 00000000000..731b0fdb286
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/process.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Process < Chain::Base
+ include Chain::Helpers
+
+ def perform!
+ raise ArgumentError, 'missing config content' unless @command.config_content
+
+ @command.config_processor = ::Gitlab::Ci::YamlProcessor.new(
+ @command.config_content, {
+ project: project,
+ sha: @pipeline.sha,
+ user: current_user
+ }
+ )
+ rescue Gitlab::Ci::YamlProcessor::ValidationError => ex
+ error(ex.message, config_error: true)
+ rescue => ex
+ Gitlab::Sentry.track_acceptable_exception(ex, extra: {
+ project_id: project.id,
+ sha: @pipeline.sha
+ })
+
+ error("Undefined error (#{Labkit::Correlation::CorrelationId.current_id})",
+ config_error: true)
+ end
+
+ def break?
+ @pipeline.errors.any? || @pipeline.persisted?
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
new file mode 100644
index 00000000000..0ee9485eebc
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class EvaluateWorkflowRules < Chain::Base
+ include ::Gitlab::Utils::StrongMemoize
+ include Chain::Helpers
+
+ def perform!
+ return unless Feature.enabled?(:workflow_rules, @pipeline.project)
+
+ unless workflow_passed?
+ error('Pipeline filtered out by workflow rules.')
+ end
+ end
+
+ def break?
+ return false unless Feature.enabled?(:workflow_rules, @pipeline.project)
+
+ !workflow_passed?
+ end
+
+ private
+
+ def workflow_passed?
+ strong_memoize(:workflow_passed) do
+ workflow_rules.evaluate(@pipeline, global_context).pass?
+ end
+ end
+
+ def workflow_rules
+ Gitlab::Ci::Build::Rules.new(
+ workflow_config[:rules], default_when: 'always')
+ end
+
+ def global_context
+ Gitlab::Ci::Build::Context::Global.new(
+ @pipeline, yaml_variables: workflow_config[:yaml_variables])
+ end
+
+ def workflow_config
+ @command.config_processor.workflow_attributes || {}
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index 13eca5a9d28..3a40c7b167c 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -10,29 +10,12 @@ module Gitlab
PopulateError = Class.new(StandardError)
def perform!
- # Allocate next IID. This operation must be outside of transactions of pipeline creations.
- pipeline.ensure_project_iid!
-
- # Protect the pipeline. This is assigned in Populate instead of
- # Build to prevent erroring out on ambiguous refs.
- pipeline.protected = @command.protected_ref?
-
- ##
- # Populate pipeline with block argument of CreatePipelineService#execute.
- #
- @command.seeds_block&.call(pipeline)
-
- ##
- # Gather all runtime build/stage errors
- #
- if seeds_errors = pipeline.stage_seeds.flat_map(&:errors).compact.presence
- return error(seeds_errors.join("\n"), config_error: true)
- end
+ raise ArgumentError, 'missing stage seeds' unless @command.stage_seeds
##
# Populate pipeline with all stages, and stages with builds.
#
- pipeline.stages = pipeline.stage_seeds.map(&:to_resource)
+ pipeline.stages = @command.stage_seeds.map(&:to_resource)
if pipeline.stages.none?
return error('No stages / jobs for this pipeline.')
diff --git a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb
index 1e09b417311..9267c72efa4 100644
--- a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb
+++ b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb
@@ -6,11 +6,13 @@ module Gitlab
module Chain
class RemoveUnwantedChatJobs < Chain::Base
def perform!
- return unless pipeline.config_processor && pipeline.chat?
+ raise ArgumentError, 'missing config processor' unless @command.config_processor
+
+ return unless pipeline.chat?
# When scheduling a chat pipeline we only want to run the build
# that matches the chat command.
- pipeline.config_processor.jobs.select! do |name, _|
+ @command.config_processor.jobs.select! do |name, _|
name.to_s == command.chat_data[:command].to_s
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/seed.rb b/lib/gitlab/ci/pipeline/chain/seed.rb
new file mode 100644
index 00000000000..2e177cfec7e
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/seed.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class Seed < Chain::Base
+ include Chain::Helpers
+ include Gitlab::Utils::StrongMemoize
+
+ def perform!
+ raise ArgumentError, 'missing config processor' unless @command.config_processor
+
+ # Allocate next IID. This operation must be outside of transactions of pipeline creations.
+ pipeline.ensure_project_iid!
+
+ # Protect the pipeline. This is assigned in Populate instead of
+ # Build to prevent erroring out on ambiguous refs.
+ pipeline.protected = @command.protected_ref?
+
+ ##
+ # Populate pipeline with block argument of CreatePipelineService#execute.
+ #
+ @command.seeds_block&.call(pipeline)
+
+ ##
+ # Gather all runtime build/stage errors
+ #
+ if stage_seeds_errors
+ return error(stage_seeds_errors.join("\n"), config_error: true)
+ end
+
+ @command.stage_seeds = stage_seeds
+ end
+
+ def break?
+ pipeline.errors.any?
+ end
+
+ private
+
+ def stage_seeds_errors
+ stage_seeds.flat_map(&:errors).compact.presence
+ end
+
+ def stage_seeds
+ strong_memoize(:stage_seeds) do
+ seeds = stages_attributes.inject([]) do |previous_stages, attributes|
+ seed = Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, attributes, previous_stages)
+ previous_stages + [seed]
+ end
+
+ seeds.select(&:included?)
+ end
+ end
+
+ def stages_attributes
+ @command.config_processor.stages_attributes
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/config.rb b/lib/gitlab/ci/pipeline/chain/validate/config.rb
deleted file mode 100644
index 28c38cc3d18..00000000000
--- a/lib/gitlab/ci/pipeline/chain/validate/config.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- module Pipeline
- module Chain
- module Validate
- class Config < Chain::Base
- include Chain::Helpers
-
- def perform!
- unless @pipeline.config_processor
- unless @pipeline.ci_yaml_file
- return error("Missing #{@pipeline.ci_yaml_file_path} file")
- end
-
- if @command.save_incompleted && @pipeline.has_yaml_errors?
- @pipeline.drop!(:config_error)
- end
-
- error(@pipeline.yaml_errors)
- end
- end
-
- def break?
- @pipeline.errors.any? || @pipeline.persisted?
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index fc9c540088b..dce56b22666 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -28,7 +28,9 @@ module Gitlab
@except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except))
@rules = Gitlab::Ci::Build::Rules
- .new(attributes.delete(:rules))
+ .new(attributes.delete(:rules), default_when: 'on_success')
+ @cache = Seed::Build::Cache
+ .new(pipeline, attributes.delete(:cache))
end
def name
@@ -38,7 +40,7 @@ module Gitlab
def included?
strong_memoize(:inclusion) do
if @using_rules
- included_by_rules?
+ rules_result.pass?
elsif @using_only || @using_except
all_of_only? && none_of_except?
else
@@ -59,6 +61,7 @@ module Gitlab
@seed_attributes
.deep_merge(pipeline_attributes)
.deep_merge(rules_attributes)
+ .deep_merge(cache_attributes)
end
def bridge?
@@ -80,26 +83,14 @@ module Gitlab
end
end
- def scoped_variables_hash
- strong_memoize(:scoped_variables_hash) do
- # This is a temporary piece of technical debt to allow us access
- # to the CI variables to evaluate rules before we persist a Build
- # with the result. We should refactor away the extra Build.new,
- # but be able to get CI Variables directly from the Seed::Build.
- ::Ci::Build.new(
- @seed_attributes.merge(pipeline_attributes)
- ).scoped_variables_hash
- end
- end
-
private
def all_of_only?
- @only.all? { |spec| spec.satisfied_by?(@pipeline, self) }
+ @only.all? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
end
def none_of_except?
- @except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
+ @except.none? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
end
def needs_errors
@@ -141,13 +132,27 @@ module Gitlab
}
end
- def included_by_rules?
- rules_attributes[:when] != 'never'
+ def rules_attributes
+ return {} unless @using_rules
+
+ rules_result.build_attributes
end
- def rules_attributes
- strong_memoize(:rules_attributes) do
- @using_rules ? @rules.evaluate(@pipeline, self).build_attributes : {}
+ def rules_result
+ strong_memoize(:rules_result) do
+ @rules.evaluate(@pipeline, evaluate_context)
+ end
+ end
+
+ def evaluate_context
+ strong_memoize(:evaluate_context) do
+ Gitlab::Ci::Build::Context::Build.new(@pipeline, @seed_attributes)
+ end
+ end
+
+ def cache_attributes
+ strong_memoize(:cache_attributes) do
+ @cache.build_attributes
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/build/cache.rb b/lib/gitlab/ci/pipeline/seed/build/cache.rb
new file mode 100644
index 00000000000..7671035b896
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/seed/build/cache.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Seed
+ class Build
+ class Cache
+ def initialize(pipeline, cache)
+ @pipeline = pipeline
+ local_cache = cache.to_h.deep_dup
+ @key = local_cache.delete(:key)
+ @paths = local_cache.delete(:paths)
+ @policy = local_cache.delete(:policy)
+ @untracked = local_cache.delete(:untracked)
+
+ raise ArgumentError, "unknown cache keys: #{local_cache.keys}" if local_cache.any?
+ end
+
+ def build_attributes
+ {
+ options: {
+ cache: {
+ key: key_string,
+ paths: @paths,
+ policy: @policy,
+ untracked: @untracked
+ }.compact.presence
+ }.compact
+ }
+ end
+
+ private
+
+ def key_string
+ key_from_string || key_from_files
+ end
+
+ def key_from_string
+ @key.to_s if @key.is_a?(String) || @key.is_a?(Symbol)
+ end
+
+ def key_from_files
+ return unless @key.is_a?(Hash)
+
+ [@key[:prefix], files_digest].select(&:present?).join('-')
+ end
+
+ def files_digest
+ hash_of_the_latest_changes || 'default'
+ end
+
+ def hash_of_the_latest_changes
+ return unless Feature.enabled?(:ci_file_based_cache, @pipeline.project, default_enabled: true)
+
+ ids = files.map { |path| last_commit_id_for_path(path) }
+ ids = ids.compact.sort.uniq
+
+ Digest::SHA1.hexdigest(ids.join('-')) if ids.any?
+ end
+
+ def files
+ @key[:files]
+ .to_a
+ .select(&:present?)
+ .uniq
+ end
+
+ def last_commit_id_for_path(path)
+ @pipeline.project.repository.last_commit_id_for_path(@pipeline.sha, path)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index 961012c2cee..910d93f54ce 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -16,7 +16,9 @@ module Gitlab
stale_schedule: 'stale schedule',
job_execution_timeout: 'job execution timeout',
archived_failure: 'archived failure',
- unmet_prerequisites: 'unmet prerequisites'
+ unmet_prerequisites: 'unmet prerequisites',
+ scheduler_failure: 'scheduler failure',
+ data_integrity_failure: 'data integrity failure'
}.freeze
private_constant :REASONS
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
index 3cdb7b5420c..a60b00b2ee8 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -18,7 +18,7 @@ code_quality:
--env SOURCE_CODE="$PWD"
--volume "$PWD":/code
--volume /var/run/docker.sock:/var/run/docker.sock
- "registry.gitlab.com/gitlab-org/security-products/codequality:12-0-stable" /code
+ "registry.gitlab.com/gitlab-org/security-products/codequality:12-5-stable" /code
artifacts:
reports:
codequality: gl-code-quality-report.json
diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
index ae2ff9992f9..7a672f910dd 100644
--- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
@@ -1,8 +1,8 @@
-.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.1.0"
+.dast-auto-deploy:
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.6.0"
dast_environment_deploy:
- extends: .auto-deploy
+ extends: .dast-auto-deploy
stage: review
script:
- auto-deploy check_kube_domain
@@ -28,10 +28,10 @@ dast_environment_deploy:
variables:
- $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME
- $DAST_DISABLED || $DAST_DISABLED_FOR_DEFAULT_BRANCH
- - $DAST_WEBSITE # we don't need to create a review app if a URL is already given
+ - $DAST_WEBSITE # we don't need to create a review app if a URL is already given
stop_dast_environment:
- extends: .auto-deploy
+ extends: .dast-auto-deploy
stage: cleanup
variables:
GIT_STRATEGY: none
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index a8ec2d4781d..738be44d5f4 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.1.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.7.0"
review:
extends: .auto-deploy
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
index f058468ed8e..ef2fc561201 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -9,16 +9,17 @@ container_scanning:
name: registry.gitlab.com/gitlab-org/security-products/analyzers/klar:$CS_MAJOR_VERSION
entrypoint: []
variables:
- # By default, use the latest clair vulnerabilities database, however, allow it to be overridden here
- # with a specific version to provide consistency for integration testing purposes
- CLAIR_DB_IMAGE_TAG: latest
- # Override this variable in your `.gitlab-ci.yml` file and set it to `fetch` if you want to provide a `clair-whitelist.yaml` file.
- # See https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html#overriding-the-container-scanning-template
+ # By default, use the latest clair vulnerabilities database, however, allow it to be overridden here with a specific image
+ # to enable container scanning to run offline, or to provide a consistent list of vulnerabilities for integration testing purposes
+ CLAIR_DB_IMAGE_TAG: "latest"
+ CLAIR_DB_IMAGE: "arminc/clair-db:$CLAIR_DB_IMAGE_TAG"
+ # Override the GIT_STRATEGY variable in your `.gitlab-ci.yml` file and set it to `fetch` if you want to provide a `clair-whitelist.yml`
+ # file. See https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html#overriding-the-container-scanning-template
# for details
GIT_STRATEGY: none
allow_failure: true
services:
- - name: arminc/clair-db:$CLAIR_DB_IMAGE_TAG
+ - name: $CLAIR_DB_IMAGE
alias: clair-vulnerabilities-db
script:
# the kubernetes executor currently ignores the Docker image entrypoint value, so the start.sh script must
diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
index c8930bc6263..4993d22d400 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -4,6 +4,12 @@
# List of the variables: https://gitlab.com/gitlab-org/security-products/dependency-scanning#settings
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
+variables:
+ DS_ANALYZER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+ DS_DEFAULT_ANALYZERS: "gemnasium, retire.js, gemnasium-python, gemnasium-maven, bundler-audit"
+ DS_MAJOR_VERSION: 2
+ DS_DISABLE_DIND: "false"
+
dependency_scanning:
stage: test
image: docker:stable
@@ -45,6 +51,7 @@ dependency_scanning:
DS_PIP_DEPENDENCY_PATH \
PIP_INDEX_URL \
PIP_EXTRA_INDEX_URL \
+ MAVEN_CLI_OPTS \
) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
@@ -61,3 +68,63 @@ dependency_scanning:
except:
variables:
- $DEPENDENCY_SCANNING_DISABLED
+ - $DS_DISABLE_DIND == 'true'
+
+.analyzer:
+ extends: dependency_scanning
+ services: []
+ except:
+ variables:
+ - $DS_DISABLE_DIND == 'false'
+ script:
+ - /analyzer run
+
+gemnasium-dependency_scanning:
+ extends: .analyzer
+ image:
+ name: "$DS_ANALYZER_IMAGE_PREFIX/gemnasium:$DS_MAJOR_VERSION"
+ only:
+ variables:
+ - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $DS_DEFAULT_ANALYZERS =~ /gemnasium/ &&
+ $CI_PROJECT_REPOSITORY_LANGUAGES =~ /ruby|javascript|php/
+
+gemnasium-maven-dependency_scanning:
+ extends: .analyzer
+ image:
+ name: "$DS_ANALYZER_IMAGE_PREFIX/gemnasium-maven:$DS_MAJOR_VERSION"
+ only:
+ variables:
+ - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $DS_DEFAULT_ANALYZERS =~ /gemnasium-maven/ &&
+ $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bjava\b/
+
+gemnasium-python-dependency_scanning:
+ extends: .analyzer
+ image:
+ name: "$DS_ANALYZER_IMAGE_PREFIX/gemnasium-python:$DS_MAJOR_VERSION"
+ only:
+ variables:
+ - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $DS_DEFAULT_ANALYZERS =~ /gemnasium-python/ &&
+ $CI_PROJECT_REPOSITORY_LANGUAGES =~ /python/
+
+bundler-audit-dependency_scanning:
+ extends: .analyzer
+ image:
+ name: "$DS_ANALYZER_IMAGE_PREFIX/bundler-audit:$DS_MAJOR_VERSION"
+ only:
+ variables:
+ - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $DS_DEFAULT_ANALYZERS =~ /bundler-audit/ &&
+ $CI_PROJECT_REPOSITORY_LANGUAGES =~ /ruby/
+
+retire-js-dependency_scanning:
+ extends: .analyzer
+ image:
+ name: "$DS_ANALYZER_IMAGE_PREFIX/retire.js:$DS_MAJOR_VERSION"
+ only:
+ variables:
+ - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $DS_DEFAULT_ANALYZERS =~ /retire.js/ &&
+ $CI_PROJECT_REPOSITORY_LANGUAGES =~ /javascript/
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index a0c2ab3aa26..c81b4efddbc 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -7,7 +7,7 @@
variables:
SAST_ANALYZER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, tslint, secrets, sobelow, pmd-apex"
- SAST_MAJOR_VERSION: 2
+ SAST_ANALYZER_IMAGE_TAG: 2
SAST_DISABLE_DIND: "false"
sast:
@@ -35,45 +35,12 @@ sast:
export DOCKER_HOST='tcp://localhost:2375'
fi
fi
- - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
- function propagate_env_vars() {
- CURRENT_ENV=$(printenv)
-
- for VAR_NAME; do
- echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
- done
- }
+ - |
+ printenv | grep -E '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | cut -d'=' -f1 | \
+ (while IFS='\\n' read -r VAR; do unset -v "$VAR"; done; /bin/printenv > .env)
- |
docker run \
- $(propagate_env_vars \
- SAST_BANDIT_EXCLUDED_PATHS \
- SAST_ANALYZER_IMAGES \
- SAST_ANALYZER_IMAGE_PREFIX \
- SAST_ANALYZER_IMAGE_TAG \
- SAST_DEFAULT_ANALYZERS \
- SAST_PULL_ANALYZER_IMAGES \
- SAST_BRAKEMAN_LEVEL \
- SAST_FLAWFINDER_LEVEL \
- SAST_GITLEAKS_ENTROPY_LEVEL \
- SAST_GOSEC_LEVEL \
- SAST_EXCLUDED_PATHS \
- SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
- SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
- SAST_RUN_ANALYZER_TIMEOUT \
- SAST_JAVA_VERSION \
- ANT_HOME \
- ANT_PATH \
- GRADLE_PATH \
- JAVA_OPTS \
- JAVA_PATH \
- JAVA_8_VERSION \
- JAVA_11_VERSION \
- MAVEN_CLI_OPTS \
- MAVEN_PATH \
- MAVEN_REPO_PATH \
- SBT_PATH \
- FAIL_NEVER \
- ) \
+ --env-file .env \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code
@@ -94,7 +61,7 @@ sast:
bandit-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/bandit:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/bandit:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -104,7 +71,7 @@ bandit-sast:
brakeman-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/brakeman:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -114,7 +81,7 @@ brakeman-sast:
eslint-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/eslint:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -124,7 +91,7 @@ eslint-sast:
flawfinder-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/flawfinder:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/flawfinder:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -134,7 +101,7 @@ flawfinder-sast:
gosec-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/gosec:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/gosec:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -144,7 +111,7 @@ gosec-sast:
nodejs-scan-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/nodejs-scan:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -154,7 +121,7 @@ nodejs-scan-sast:
phpcs-security-audit-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/phpcs-security-audit:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/phpcs-security-audit:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -164,7 +131,7 @@ phpcs-security-audit-sast:
pmd-apex-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/pmd-apex:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/pmd-apex:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -174,7 +141,7 @@ pmd-apex-sast:
secrets-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/secrets:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/secrets:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -183,7 +150,7 @@ secrets-sast:
security-code-scan-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/security-code-scan:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/security-code-scan:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -193,7 +160,7 @@ security-code-scan-sast:
sobelow-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/sobelow:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/sobelow:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -203,7 +170,7 @@ sobelow-sast:
spotbugs-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/spotbugs:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -213,7 +180,7 @@ spotbugs-sast:
tslint-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/tslint:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/tslint:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index f6a3abefcfb..833c545fc5b 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -39,15 +39,15 @@ module Gitlab
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
- yaml_variables: yaml_variables(name),
- needs_attributes: job[:needs]&.map { |need| { name: need } },
+ yaml_variables: transform_to_yaml_variables(job_variables(name)),
+ needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible],
rules: job[:rules],
+ cache: job[:cache],
options: {
image: job[:image],
services: job[:services],
artifacts: job[:artifacts],
- cache: job[:cache],
dependencies: job[:dependencies],
job_timeout: job[:timeout],
before_script: job[:before_script],
@@ -59,7 +59,7 @@ module Gitlab
instance: job[:instance],
start_in: job[:start_in],
trigger: job[:trigger],
- bridge_needs: job[:needs]
+ bridge_needs: job.dig(:needs, :bridge)&.first
}.compact }.compact
end
@@ -83,6 +83,13 @@ module Gitlab
end
end
+ def workflow_attributes
+ {
+ rules: @config.dig(:workflow, :rules),
+ yaml_variables: transform_to_yaml_variables(@variables)
+ }
+ end
+
def self.validation_message(content, opts = {})
return 'Please provide content of .gitlab-ci.yml' if content.blank?
@@ -118,20 +125,17 @@ module Gitlab
end
end
- def yaml_variables(name)
- variables = (@variables || {})
- .merge(job_variables(name))
+ def job_variables(name)
+ job_variables = @jobs.dig(name.to_sym, :variables)
- variables.map do |key, value|
- { key: key.to_s, value: value, public: true }
- end
+ @variables.to_h
+ .merge(job_variables.to_h)
end
- def job_variables(name)
- job = @jobs[name.to_sym]
- return {} unless job
-
- job[:variables] || {}
+ def transform_to_yaml_variables(variables)
+ variables.to_h.map do |key, value|
+ { key: key.to_s, value: value, public: true }
+ end
end
def validate_job_stage!(name, job)
@@ -159,17 +163,19 @@ module Gitlab
end
def validate_job_needs!(name, job)
- return unless job[:needs]
+ return unless job.dig(:needs, :job)
stage_index = @stages.index(job[:stage])
- job[:needs].each do |need|
- raise ValidationError, "#{name} job: undefined need: #{need}" unless @jobs[need.to_sym]
+ job.dig(:needs, :job).each do |need|
+ need_job_name = need[:name]
+
+ raise ValidationError, "#{name} job: undefined need: #{need_job_name}" unless @jobs[need_job_name.to_sym]
- needs_stage_index = @stages.index(@jobs[need.to_sym][:stage])
+ needs_stage_index = @stages.index(@jobs[need_job_name.to_sym][:stage])
unless needs_stage_index.present? && needs_stage_index < stage_index
- raise ValidationError, "#{name} job: need #{need} is not defined in prior stages"
+ raise ValidationError, "#{name} job: need #{need_job_name} is not defined in prior stages"
end
end
end
diff --git a/lib/gitlab/cleanup/orphan_job_artifact_files.rb b/lib/gitlab/cleanup/orphan_job_artifact_files.rb
index 1b01ca25559..020de45e5bf 100644
--- a/lib/gitlab/cleanup/orphan_job_artifact_files.rb
+++ b/lib/gitlab/cleanup/orphan_job_artifact_files.rb
@@ -8,7 +8,8 @@ module Gitlab
ABSOLUTE_ARTIFACT_DIR = ::JobArtifactUploader.root.freeze
LOST_AND_FOUND = File.join(ABSOLUTE_ARTIFACT_DIR, '-', 'lost+found').freeze
BATCH_SIZE = 500
- DEFAULT_NICENESS = 'Best-effort'
+ DEFAULT_NICENESS = 'best-effort'
+ VALID_NICENESS_LEVELS = %w{none realtime best-effort idle}.freeze
attr_accessor :batch, :total_found, :total_cleaned
attr_reader :limit, :dry_run, :niceness, :logger
@@ -16,7 +17,7 @@ module Gitlab
def initialize(limit: nil, dry_run: true, niceness: nil, logger: nil)
@limit = limit
@dry_run = dry_run
- @niceness = niceness || DEFAULT_NICENESS
+ @niceness = (niceness || DEFAULT_NICENESS).downcase
@logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger
@total_found = @total_cleaned = 0
@@ -35,7 +36,7 @@ module Gitlab
clean_batch!
- log_info("Processed #{total_found} job artifacts to find and clean #{total_cleaned} orphans.")
+ log_info("Processed #{total_found} job artifact(s) to find and cleaned #{total_cleaned} orphan(s).")
end
private
@@ -75,7 +76,7 @@ module Gitlab
def find_artifacts
Open3.popen3(*find_command) do |stdin, stdout, stderr, status_thread|
stdout.each_line do |line|
- yield line
+ yield line.chomp
end
log_error(stderr.read.color(:red)) unless status_thread.value.success?
@@ -99,7 +100,7 @@ module Gitlab
cmd += %w[-type d]
if ionice
- raise ArgumentError, 'Invalid niceness' unless niceness.match?(/^\w[\w\-]*$/)
+ raise ArgumentError, 'Invalid niceness' unless VALID_NICENESS_LEVELS.include?(niceness)
cmd.unshift(*%W[#{ionice} --class #{niceness}])
end
diff --git a/lib/gitlab/cluster/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb
index 294ffad02ce..2b3dc94fc5e 100644
--- a/lib/gitlab/cluster/lifecycle_events.rb
+++ b/lib/gitlab/cluster/lifecycle_events.rb
@@ -10,38 +10,39 @@ module Gitlab
#
# We have the following lifecycle events.
#
- # - on_master_start:
+ # - on_before_fork (on master process):
#
# Unicorn/Puma Cluster: This will be called exactly once,
# on startup, before the workers are forked. This is
# called in the PARENT/MASTER process.
#
- # Sidekiq/Puma Single: This is called immediately.
+ # Sidekiq/Puma Single: This is not called.
#
- # - on_before_fork:
+ # - on_master_start (on master process):
#
# Unicorn/Puma Cluster: This will be called exactly once,
# on startup, before the workers are forked. This is
# called in the PARENT/MASTER process.
#
- # Sidekiq/Puma Single: This is not called.
+ # Sidekiq/Puma Single: This is called immediately.
#
- # - on_worker_start:
+ # - on_before_blackout_period (on master process):
#
- # Unicorn/Puma Cluster: This is called in the worker process
- # exactly once before processing requests.
+ # Unicorn/Puma Cluster: This will be called before a blackout
+ # period when performing graceful shutdown of master.
+ # This is called on `master` process.
#
- # Sidekiq/Puma Single: This is called immediately.
+ # Sidekiq/Puma Single: This is not called.
#
- # - on_before_phased_restart:
+ # - on_before_graceful_shutdown (on master process):
#
# Unicorn/Puma Cluster: This will be called before a graceful
- # shutdown of workers starts happening.
+ # shutdown of workers starts happening, but after blackout period.
# This is called on `master` process.
#
# Sidekiq/Puma Single: This is not called.
#
- # - on_before_master_restart:
+ # - on_before_master_restart (on master process):
#
# Unicorn: This will be called before a new master is spun up.
# This is called on forked master before `execve` to become
@@ -53,6 +54,13 @@ module Gitlab
#
# Sidekiq/Puma Single: This is not called.
#
+ # - on_worker_start (on worker process):
+ #
+ # Unicorn/Puma Cluster: This is called in the worker process
+ # exactly once before processing requests.
+ #
+ # Sidekiq/Puma Single: This is called immediately.
+ #
# Blocks will be executed in the order in which they are registered.
#
class LifecycleEvents
@@ -75,9 +83,15 @@ module Gitlab
end
# Read the config/initializers/cluster_events_before_phased_restart.rb
- def on_before_phased_restart(&block)
+ def on_before_blackout_period(&block)
# Defer block execution
- (@master_phased_restart ||= []) << block
+ (@master_blackout_period ||= []) << block
+ end
+
+ # Read the config/initializers/cluster_events_before_phased_restart.rb
+ def on_before_graceful_shutdown(&block)
+ # Defer block execution
+ (@master_graceful_shutdown ||= []) << block
end
def on_before_master_restart(&block)
@@ -97,27 +111,24 @@ module Gitlab
# Lifecycle integration methods (called from unicorn.rb, puma.rb, etc.)
#
def do_worker_start
- @worker_start_hooks&.each do |block|
- block.call
- end
+ call(@worker_start_hooks)
end
def do_before_fork
- @before_fork_hooks&.each do |block|
- block.call
- end
+ call(@before_fork_hooks)
end
- def do_before_phased_restart
- @master_phased_restart&.each do |block|
- block.call
- end
+ def do_before_graceful_shutdown
+ call(@master_blackout_period)
+
+ blackout_seconds = ::Settings.shutdown.blackout_seconds.to_i
+ sleep(blackout_seconds) if blackout_seconds > 0
+
+ call(@master_graceful_shutdown)
end
def do_before_master_restart
- @master_restart_hooks&.each do |block|
- block.call
- end
+ call(@master_restart_hooks)
end
# DEPRECATED
@@ -132,6 +143,10 @@ module Gitlab
private
+ def call(hooks)
+ hooks&.each(&:call)
+ end
+
def in_clustered_environment?
# Sidekiq doesn't fork
return false if Sidekiq.server?
diff --git a/lib/gitlab/cluster/mixins/puma_cluster.rb b/lib/gitlab/cluster/mixins/puma_cluster.rb
index e9157d9f1e4..106c2731c07 100644
--- a/lib/gitlab/cluster/mixins/puma_cluster.rb
+++ b/lib/gitlab/cluster/mixins/puma_cluster.rb
@@ -8,8 +8,12 @@ module Gitlab
raise 'missing method Puma::Cluster#stop_workers' unless base.method_defined?(:stop_workers)
end
+ # This looks at internal status of `Puma::Cluster`
+ # https://github.com/puma/puma/blob/v3.12.1/lib/puma/cluster.rb#L333
def stop_workers
- Gitlab::Cluster::LifecycleEvents.do_before_phased_restart
+ if @status == :stop # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ Gitlab::Cluster::LifecycleEvents.do_before_graceful_shutdown
+ end
super
end
diff --git a/lib/gitlab/cluster/mixins/unicorn_http_server.rb b/lib/gitlab/cluster/mixins/unicorn_http_server.rb
index 765fd0c2baa..440ed02a355 100644
--- a/lib/gitlab/cluster/mixins/unicorn_http_server.rb
+++ b/lib/gitlab/cluster/mixins/unicorn_http_server.rb
@@ -5,11 +5,26 @@ module Gitlab
module Mixins
module UnicornHttpServer
def self.prepended(base)
- raise 'missing method Unicorn::HttpServer#reexec' unless base.method_defined?(:reexec)
+ unless base.method_defined?(:reexec) && base.method_defined?(:stop)
+ raise 'missing method Unicorn::HttpServer#reexec or Unicorn::HttpServer#stop'
+ end
end
def reexec
- Gitlab::Cluster::LifecycleEvents.do_before_phased_restart
+ Gitlab::Cluster::LifecycleEvents.do_before_graceful_shutdown
+
+ super
+ end
+
+ # The stop on non-graceful shutdown is executed twice:
+ # `#stop(false)` and `#stop`.
+ #
+ # The first stop will wipe-out all workers, so we need to check
+ # the flag and a list of workers
+ def stop(graceful = true)
+ if graceful && @workers.any? # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ Gitlab::Cluster::LifecycleEvents.do_before_graceful_shutdown
+ end
super
end
diff --git a/lib/gitlab/cluster/puma_worker_killer_initializer.rb b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
index a8440b63baa..92c799875b5 100644
--- a/lib/gitlab/cluster/puma_worker_killer_initializer.rb
+++ b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
@@ -3,7 +3,12 @@
module Gitlab
module Cluster
class PumaWorkerKillerInitializer
- def self.start(puma_options, puma_per_worker_max_memory_mb: 850, puma_master_max_memory_mb: 550)
+ def self.start(
+ puma_options,
+ puma_per_worker_max_memory_mb: 850,
+ puma_master_max_memory_mb: 550,
+ additional_puma_dev_max_memory_mb: 200
+ )
require 'puma_worker_killer'
PumaWorkerKiller.config do |config|
@@ -14,7 +19,11 @@ module Gitlab
# The Puma Worker Killer checks the total RAM used by both the master
# and worker processes.
# https://github.com/schneems/puma_worker_killer/blob/v0.1.0/lib/puma_worker_killer/puma_memory.rb#L57
- config.ram = puma_master_max_memory_mb + (worker_count * puma_per_worker_max_memory_mb)
+ #
+ # Additional memory is added when running in `development`
+ config.ram = puma_master_max_memory_mb +
+ (worker_count * puma_per_worker_max_memory_mb) +
+ (Rails.env.development? ? (1 + worker_count) * additional_puma_dev_max_memory_mb : 0)
config.frequency = 20 # seconds
diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb
index b7ec4b7c4f8..bda84dc2cff 100644
--- a/lib/gitlab/config/entry/configurable.rb
+++ b/lib/gitlab/config/entry/configurable.rb
@@ -29,22 +29,24 @@ module Gitlab
def compose!(deps = nil)
return unless valid?
- self.class.nodes.each do |key, factory|
- # If we override the config type validation
- # we can end with different config types like String
- next unless config.is_a?(Hash)
+ super do
+ self.class.nodes.each do |key, factory|
+ # If we override the config type validation
+ # we can end with different config types like String
+ next unless config.is_a?(Hash)
- factory
- .value(config[key])
- .with(key: key, parent: self)
+ factory
+ .value(config[key])
+ .with(key: key, parent: self)
- entries[key] = factory.create!
- end
+ entries[key] = factory.create!
+ end
- yield if block_given?
+ yield if block_given?
- entries.each_value do |entry|
- entry.compose!(deps)
+ entries.each_value do |entry|
+ entry.compose!(deps)
+ end
end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -67,12 +69,13 @@ module Gitlab
private
# rubocop: disable CodeReuse/ActiveRecord
- def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil)
+ def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil, metadata: {})
factory = ::Gitlab::Config::Entry::Factory.new(entry)
.with(description: description)
.with(default: default)
.with(inherit: inherit)
.with(reserved: reserved)
+ .metadata(metadata)
(@nodes ||= {}).merge!(key.to_sym => factory)
end
diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb
index 8f1f4a81bb5..7c5ffaa7621 100644
--- a/lib/gitlab/config/entry/factory.rb
+++ b/lib/gitlab/config/entry/factory.rb
@@ -9,10 +9,12 @@ module Gitlab
class Factory
InvalidFactory = Class.new(StandardError)
- def initialize(entry)
- @entry = entry
+ attr_reader :entry_class
+
+ def initialize(entry_class)
+ @entry_class = entry_class
@metadata = {}
- @attributes = { default: entry.default }
+ @attributes = { default: entry_class.default }
end
def value(value)
@@ -34,6 +36,10 @@ module Gitlab
@attributes[:description]
end
+ def inherit
+ @attributes[:inherit]
+ end
+
def inheritable?
@attributes[:inherit]
end
@@ -52,7 +58,7 @@ module Gitlab
if @value.nil?
Entry::Unspecified.new(fabricate_unspecified)
else
- fabricate(@entry, @value)
+ fabricate(entry_class, @value)
end
end
@@ -68,12 +74,12 @@ module Gitlab
if default.nil?
fabricate(Entry::Undefined)
else
- fabricate(@entry, default)
+ fabricate(entry_class, default)
end
end
- def fabricate(entry, value = nil)
- entry.new(value, @metadata) do |node|
+ def fabricate(entry_class, value = nil)
+ entry_class.new(value, @metadata) do |node|
node.key = @attributes[:key]
node.parent = @attributes[:parent]
node.default = @attributes[:default]
diff --git a/lib/gitlab/config/entry/inheritable.rb b/lib/gitlab/config/entry/inheritable.rb
new file mode 100644
index 00000000000..91ca82e6338
--- /dev/null
+++ b/lib/gitlab/config/entry/inheritable.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # Entry that represents an inheritable configs.
+ #
+ module Inheritable
+ InheritError = Class.new(Gitlab::Config::Loader::FormatError)
+
+ def compose!(deps = nil, &blk)
+ super(deps, &blk)
+
+ inherit!(deps)
+ end
+
+ private
+
+ # We inherit config entries from `default:`
+ # if the entry has the `inherit: true` flag set
+ def inherit!(deps)
+ return unless deps
+
+ self.class.nodes.each do |key, factory|
+ next unless factory.inheritable?
+
+ new_entry = overwrite_entry(deps, key, self[key])
+
+ entries[key] = new_entry if new_entry&.specified?
+ end
+ end
+
+ def overwrite_entry(deps, key, current_entry)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb
index e014f15fbd8..84d3409ed91 100644
--- a/lib/gitlab/config/entry/node.rb
+++ b/lib/gitlab/config/entry/node.rb
@@ -112,6 +112,10 @@ module Gitlab
@aspects ||= []
end
+ def self.with_aspect(blk)
+ self.aspects.append(blk)
+ end
+
private
attr_reader :entries
diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb
index d58aba07d15..315f1947e2c 100644
--- a/lib/gitlab/config/entry/simplifiable.rb
+++ b/lib/gitlab/config/entry/simplifiable.rb
@@ -4,11 +4,11 @@ module Gitlab
module Config
module Entry
class Simplifiable < SimpleDelegator
- EntryStrategy = Struct.new(:name, :condition)
+ EntryStrategy = Struct.new(:name, :klass, :condition)
attr_reader :subject
- def initialize(config, **metadata)
+ def initialize(config, **metadata, &blk)
unless self.class.const_defined?(:UnknownStrategy)
raise ArgumentError, 'UndefinedStrategy not available!'
end
@@ -19,14 +19,13 @@ module Gitlab
entry = self.class.entry_class(strategy)
- @subject = entry.new(config, metadata)
+ @subject = entry.new(config, metadata, &blk)
- yield(@subject) if block_given?
super(@subject)
end
def self.strategy(name, **opts)
- EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy|
+ EntryStrategy.new(name, opts.dig(:class), opts.fetch(:if)).tap do |strategy|
strategies.append(strategy)
end
end
@@ -37,7 +36,7 @@ module Gitlab
def self.entry_class(strategy)
if strategy.present?
- self.const_get(strategy.name, false)
+ strategy.klass || self.const_get(strategy.name, false)
else
self::UnknownStrategy
end
diff --git a/lib/gitlab/config/entry/validatable.rb b/lib/gitlab/config/entry/validatable.rb
index 1c88c68c11c..45b852dc2e0 100644
--- a/lib/gitlab/config/entry/validatable.rb
+++ b/lib/gitlab/config/entry/validatable.rb
@@ -7,14 +7,27 @@ module Gitlab
extend ActiveSupport::Concern
def self.included(node)
- node.aspects.append -> do
- @validator = self.class.validator.new(self)
- @validator.validate(:new)
+ node.with_aspect -> do
+ validate(:new)
end
end
+ def validator
+ @validator ||= self.class.validator.new(self)
+ end
+
+ def validate(context = nil)
+ validator.validate(context)
+ end
+
+ def compose!(deps = nil, &blk)
+ super(deps, &blk)
+
+ validate(:composed)
+ end
+
def errors
- @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ validator.messages + descendants.flat_map(&:errors)
end
class_methods do
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 374f929878e..d1c23c41d35 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -61,8 +61,15 @@ module Gitlab
include LegacyValidationHelpers
def validate_each(record, attribute, value)
- unless validate_array_of_strings(value)
- record.errors.add(attribute, 'should be an array of strings')
+ valid = validate_array_of_strings(value)
+
+ record.errors.add(attribute, 'should be an array of strings') unless valid
+
+ if valid && options[:with]
+ unless value.all? { |v| v =~ options[:with] }
+ message = options[:message] || 'contains elements that do not match the format'
+ record.errors.add(attribute, message)
+ end
end
end
end
@@ -221,6 +228,34 @@ module Gitlab
end
end
+ class NestedArrayOfStringsValidator < ArrayOfStringsOrStringValidator
+ def validate_each(record, attribute, value)
+ unless validate_nested_array_of_strings(value)
+ record.errors.add(attribute, 'should be an array containing strings and arrays of strings')
+ end
+ end
+
+ private
+
+ def validate_nested_array_of_strings(values)
+ values.is_a?(Array) && values.all? { |element| validate_array_of_strings_or_string(element) }
+ end
+ end
+
+ class StringOrNestedArrayOfStringsValidator < NestedArrayOfStringsValidator
+ def validate_each(record, attribute, value)
+ unless validate_string_or_nested_array_of_strings(value)
+ record.errors.add(attribute, 'should be a string or an array containing strings and arrays of strings')
+ end
+ end
+
+ private
+
+ def validate_string_or_nested_array_of_strings(values)
+ validate_string(values) || validate_nested_array_of_strings(values)
+ end
+ end
+
class TypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
type = options[:with]
diff --git a/lib/gitlab/cycle_analytics/group_stage_summary.rb b/lib/gitlab/cycle_analytics/group_stage_summary.rb
index a1fc941495d..26eaaf7df83 100644
--- a/lib/gitlab/cycle_analytics/group_stage_summary.rb
+++ b/lib/gitlab/cycle_analytics/group_stage_summary.rb
@@ -3,18 +3,17 @@
module Gitlab
module CycleAnalytics
class GroupStageSummary
- attr_reader :group, :from, :current_user, :options
+ attr_reader :group, :current_user, :options
def initialize(group, options:)
@group = group
- @from = options[:from]
@current_user = options[:current_user]
@options = options
end
def data
- [serialize(Summary::Group::Issue.new(group: group, from: from, current_user: current_user, options: options)),
- serialize(Summary::Group::Deploy.new(group: group, from: from, options: options))]
+ [serialize(Summary::Group::Issue.new(group: group, current_user: current_user, options: options)),
+ serialize(Summary::Group::Deploy.new(group: group, options: options))]
end
private
diff --git a/lib/gitlab/cycle_analytics/summary/group/base.rb b/lib/gitlab/cycle_analytics/summary/group/base.rb
index 48d8164bde1..f1d20d5aefa 100644
--- a/lib/gitlab/cycle_analytics/summary/group/base.rb
+++ b/lib/gitlab/cycle_analytics/summary/group/base.rb
@@ -5,11 +5,10 @@ module Gitlab
module Summary
module Group
class Base
- attr_reader :group, :from, :options
+ attr_reader :group, :options
- def initialize(group:, from:, options:)
+ def initialize(group:, options:)
@group = group
- @from = from
@options = options
end
diff --git a/lib/gitlab/cycle_analytics/summary/group/deploy.rb b/lib/gitlab/cycle_analytics/summary/group/deploy.rb
index 78d677cf558..11a9152cf0c 100644
--- a/lib/gitlab/cycle_analytics/summary/group/deploy.rb
+++ b/lib/gitlab/cycle_analytics/summary/group/deploy.rb
@@ -20,7 +20,8 @@ module Gitlab
def find_deployments
deployments = Deployment.joins(:project).merge(Project.inside_path(group.full_path))
deployments = deployments.where(projects: { id: options[:projects] }) if options[:projects]
- deployments = deployments.where("deployments.created_at > ?", from)
+ deployments = deployments.where("deployments.created_at > ?", options[:from])
+ deployments = deployments.where("deployments.created_at < ?", options[:to]) if options[:to]
deployments.success.count
end
end
diff --git a/lib/gitlab/cycle_analytics/summary/group/issue.rb b/lib/gitlab/cycle_analytics/summary/group/issue.rb
index 9daae8531d8..4d5ee1d43ca 100644
--- a/lib/gitlab/cycle_analytics/summary/group/issue.rb
+++ b/lib/gitlab/cycle_analytics/summary/group/issue.rb
@@ -5,11 +5,10 @@ module Gitlab
module Summary
module Group
class Issue < Group::Base
- attr_reader :group, :from, :current_user, :options
+ attr_reader :group, :current_user, :options
- def initialize(group:, from:, current_user:, options:)
+ def initialize(group:, current_user:, options:)
@group = group
- @from = from
@current_user = current_user
@options = options
end
@@ -25,10 +24,19 @@ module Gitlab
private
def find_issues
- issues = IssuesFinder.new(current_user, group_id: group.id, include_subgroups: true, created_after: from).execute
+ issues = IssuesFinder.new(current_user, finder_params).execute
issues = issues.where(projects: { id: options[:projects] }) if options[:projects]
issues.count
end
+
+ def finder_params
+ {
+ group_id: group.id,
+ include_subgroups: true,
+ created_after: options[:from],
+ created_before: options[:to]
+ }.compact
+ end
end
end
end
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
index 8a253893892..ddb9d907640 100644
--- a/lib/gitlab/daemon.rb
+++ b/lib/gitlab/daemon.rb
@@ -28,6 +28,10 @@ module Gitlab
true
end
+ def thread_name
+ self.class.name.demodulize.underscore
+ end
+
def start
return unless enabled?
@@ -35,7 +39,10 @@ module Gitlab
break thread if thread?
if start_working
- @thread = Thread.new { run_thread }
+ @thread = Thread.new do
+ Thread.current.name = thread_name
+ run_thread
+ end
end
end
end
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index f22fc41a6d8..0e7e0c40a8a 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -93,8 +93,8 @@ module Gitlab
docs: "~documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
none: "",
qa: "~QA",
- test: "~test for `spec/features/*`",
- engineering_productivity: "Engineering Productivity for CI config review"
+ test: "~test ~Quality for `spec/features/*`",
+ engineering_productivity: '~"Engineering Productivity" for CI, Danger'
}.freeze
CATEGORIES = {
%r{\Adoc/} => :none, # To reinstate roulette for documentation, set to `:docs`.
@@ -104,7 +104,7 @@ module Gitlab
%r{\A(ee/)?public/} => :frontend,
%r{\A(ee/)?spec/(javascripts|frontend)/} => :frontend,
%r{\A(ee/)?vendor/assets/} => :frontend,
- %r{\Ascripts/frontend/} => :frontend,
+ %r{\A(ee/)?scripts/frontend/} => :frontend,
%r{(\A|/)(
\.babelrc |
\.eslintignore |
@@ -130,14 +130,18 @@ module Gitlab
%r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => :database,
%r{\Arubocop/cop/migration(/|\.rb)} => :database,
+ %r{\A(\.gitlab-ci\.yml\z|\.gitlab\/ci)} => :engineering_productivity,
+ %r{Dangerfile\z} => :engineering_productivity,
+ %r{\A(ee/)?(danger/|lib/gitlab/danger/)} => :engineering_productivity,
+ %r{\A(ee/)?scripts/} => :engineering_productivity,
+
%r{\A(ee/)?app/(?!assets|views)[^/]+} => :backend,
- %r{\A(ee/)?(bin|config|danger|generator_templates|lib|rubocop|scripts)/} => :backend,
+ %r{\A(ee/)?(bin|config|generator_templates|lib|rubocop)/} => :backend,
%r{\A(ee/)?spec/features/} => :test,
%r{\A(ee/)?spec/(?!javascripts|frontend)[^/]+} => :backend,
%r{\A(ee/)?vendor/(?!assets)[^/]+} => :backend,
%r{\A(ee/)?vendor/(languages\.yml|licenses\.csv)\z} => :backend,
- %r{\A(\.gitlab-ci\.yml\z|\.gitlab\/ci)} => :engineering_productivity,
- %r{\A(Dangerfile|Gemfile|Gemfile.lock|Procfile|Rakefile)\z} => :backend,
+ %r{\A(Gemfile|Gemfile.lock|Procfile|Rakefile)\z} => :backend,
%r{\A[A-Z_]+_VERSION\z} => :backend,
%r{\A\.rubocop(_todo)?\.yml\z} => :backend,
diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb
index 5c2324836d7..e96f5177195 100644
--- a/lib/gitlab/danger/teammate.rb
+++ b/lib/gitlab/danger/teammate.rb
@@ -67,7 +67,10 @@ module Gitlab
area && labels.any?("devops::#{area.downcase}") if kind == :reviewer
when :engineering_productivity
- role[/Engineering Productivity/] if kind == :reviewer
+ return false unless role[/Engineering Productivity/]
+ return true if kind == :reviewer
+
+ capabilities(project).include?("#{kind} backend")
else
capabilities(project).include?("#{kind} #{category}")
end
diff --git a/lib/gitlab/data_builder/deployment.rb b/lib/gitlab/data_builder/deployment.rb
index f11e032ab84..70587b3132a 100644
--- a/lib/gitlab/data_builder/deployment.rb
+++ b/lib/gitlab/data_builder/deployment.rb
@@ -6,11 +6,17 @@ module Gitlab
extend self
def build(deployment)
+ # Deployments will not have a deployable when created using the API.
+ deployable_url =
+ if deployment.deployable
+ Gitlab::UrlBuilder.build(deployment.deployable)
+ end
+
{
object_kind: 'deployment',
status: deployment.status,
deployable_id: deployment.deployable_id,
- deployable_url: Gitlab::UrlBuilder.build(deployment.deployable),
+ deployable_url: deployable_url,
environment: deployment.environment.name,
project: deployment.project.hook_attrs,
short_sha: deployment.short_sha,
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index a83b03f540c..65cfd47e1e8 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -19,12 +19,25 @@ module Gitlab
user_email: "john@example.com",
user_avatar: "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
project_id: 15,
+ project: {
+ id: 15,
+ name: "gitlab",
+ description: "",
+ web_url: "http://test.example.com/gitlab/gitlab",
+ avatar_url: "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ git_ssh_url: "git@test.example.com:gitlab/gitlab.git",
+ git_http_url: "http://test.example.com/gitlab/gitlab.git",
+ namespace: "gitlab",
+ visibility_level: 0,
+ path_with_namespace: "gitlab/gitlab",
+ default_branch: "master"
+ },
commits: [
{
id: "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
message: "Add simple search to projects in public area",
timestamp: "2013-05-13T18:18:08+00:00",
- url: "https://test.example.com/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ url: "https://test.example.com/gitlab/gitlab/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
author: {
name: "Test User",
email: "test@example.com"
@@ -45,7 +58,20 @@ module Gitlab
# user_name: String,
# user_username: String,
# user_email: String
- # project_id: String,
+ # project_id: Fixnum,
+ # project: {
+ # id: Fixnum,
+ # name: String,
+ # description: String,
+ # web_url: String,
+ # avatar_url: String,
+ # git_ssh_url: String,
+ # git_http_url: String,
+ # namespace: String,
+ # visibility_level: Fixnum,
+ # path_with_namespace: String,
+ # default_branch: String
+ # }
# repository: {
# name: String,
# url: String,
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index ae29546cdac..7ea7565f758 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -108,9 +108,7 @@ module Gitlab
'in the body of your migration class'
end
- if supports_drop_index_concurrently?
- options = options.merge({ algorithm: :concurrently })
- end
+ options = options.merge({ algorithm: :concurrently })
unless index_exists?(table_name, column_name, options)
Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}" # rubocop:disable Gitlab/RailsLogger
@@ -136,9 +134,7 @@ module Gitlab
'in the body of your migration class'
end
- if supports_drop_index_concurrently?
- options = options.merge({ algorithm: :concurrently })
- end
+ options = options.merge({ algorithm: :concurrently })
unless index_exists_by_name?(table_name, index_name)
Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, index_name: #{index_name}" # rubocop:disable Gitlab/RailsLogger
@@ -150,13 +146,6 @@ module Gitlab
end
end
- # Only available on Postgresql >= 9.2
- def supports_drop_index_concurrently?
- version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
-
- version >= 90200
- end
-
# Adds a foreign key with only minimal locking on the tables involved.
#
# This method only requires minimal locking
@@ -966,7 +955,7 @@ into similar problems in the future (e.g. when new tables are created).
table_name = model_class.quoted_table_name
model_class.each_batch(of: batch_size) do |relation|
- start_id, end_id = relation.pluck("MIN(#{table_name}.id), MAX(#{table_name}.id)").first
+ start_id, end_id = relation.pluck("MIN(#{table_name}.id)", "MAX(#{table_name}.id)").first
if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE
# Note: This code path generally only helps with many millions of rows
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
index 3e8a9b89998..cea25967801 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
@@ -66,11 +66,13 @@ module Gitlab
def move_repositories(namespace, old_full_path, new_full_path)
repo_shards_for_namespace(namespace).each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage, old_full_path)
+ Gitlab::GitalyClient::NamespaceService.allow do
+ gitlab_shell.add_namespace(repository_storage, old_full_path)
- unless gitlab_shell.mv_namespace(repository_storage, old_full_path, new_full_path)
- message = "Exception moving on shard #{repository_storage} from #{old_full_path} to #{new_full_path}"
- Rails.logger.error message # rubocop:disable Gitlab/RailsLogger
+ unless gitlab_shell.mv_namespace(repository_storage, old_full_path, new_full_path)
+ message = "Exception moving on shard #{repository_storage} from #{old_full_path} to #{new_full_path}"
+ Rails.logger.error message # rubocop:disable Gitlab/RailsLogger
+ end
end
end
end
diff --git a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
index dfef158cc1d..8cd9694b741 100644
--- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
+++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
@@ -21,7 +21,6 @@ module Gitlab
:create_project,
:save_project_id,
:add_group_members,
- :add_to_whitelist,
:add_prometheus_manual_configuration
def initialize
@@ -126,28 +125,6 @@ module Gitlab
end
end
- def add_to_whitelist(result)
- return success(result) unless prometheus_enabled?
- return success(result) unless prometheus_listen_address.present?
-
- uri = parse_url(internal_prometheus_listen_address_uri)
- return error(_('Prometheus listen_address in config/gitlab.yml is not a valid URI')) unless uri
-
- application_settings.add_to_outbound_local_requests_whitelist([uri.normalized_host])
- response = application_settings.save
-
- if response
- # Expire the Gitlab::CurrentSettings cache after updating the whitelist.
- # This happens automatically in an after_commit hook, but in migrations,
- # the after_commit hook only runs at the end of the migration.
- Gitlab::CurrentSettings.expire_current_application_settings
- success(result)
- else
- log_error("Could not add prometheus URL to whitelist, errors: %{errors}" % { errors: application_settings.errors.full_messages })
- error(_('Could not add prometheus URL to whitelist'))
- end
- end
-
def add_prometheus_manual_configuration(result)
return success(result) unless prometheus_enabled?
return success(result) unless prometheus_listen_address.present?
@@ -176,19 +153,11 @@ module Gitlab
end
def prometheus_enabled?
- Gitlab.config.prometheus.enable if Gitlab.config.prometheus
- rescue Settingslogic::MissingSetting
- log_error('prometheus.enable is not present in config/gitlab.yml')
-
- false
+ ::Gitlab::Prometheus::Internal.prometheus_enabled?
end
def prometheus_listen_address
- Gitlab.config.prometheus.listen_address.to_s if Gitlab.config.prometheus
- rescue Settingslogic::MissingSetting
- log_error('Prometheus listen_address is not present in config/gitlab.yml')
-
- nil
+ ::Gitlab::Prometheus::Internal.listen_address
end
def instance_admins
@@ -231,23 +200,7 @@ module Gitlab
end
def internal_prometheus_listen_address_uri
- if prometheus_listen_address.starts_with?('0.0.0.0:')
- # 0.0.0.0:9090
- port = ':' + prometheus_listen_address.split(':').second
- 'http://localhost' + port
-
- elsif prometheus_listen_address.starts_with?(':')
- # :9090
- 'http://localhost' + prometheus_listen_address
-
- elsif prometheus_listen_address.starts_with?('http')
- # https://localhost:9090
- prometheus_listen_address
-
- else
- # localhost:9090
- 'http://' + prometheus_listen_address
- end
+ ::Gitlab::Prometheus::Internal.uri
end
def prometheus_service_attributes
diff --git a/lib/gitlab/devise_failure.rb b/lib/gitlab/devise_failure.rb
index 4d27b706e1e..59a7c4a6660 100644
--- a/lib/gitlab/devise_failure.rb
+++ b/lib/gitlab/devise_failure.rb
@@ -2,6 +2,8 @@
module Gitlab
class DeviseFailure < Devise::FailureApp
+ include ::SessionsHelper
+
# If the request format is not known, send a redirect instead of a 401
# response, since this is the outcome we're most likely to want
def http_auth?
@@ -9,5 +11,11 @@ module Gitlab
request_format && super
end
+
+ def respond
+ limit_session_time
+
+ super
+ end
end
end
diff --git a/lib/gitlab/error_tracking/detailed_error.rb b/lib/gitlab/error_tracking/detailed_error.rb
new file mode 100644
index 00000000000..225280a42f4
--- /dev/null
+++ b/lib/gitlab/error_tracking/detailed_error.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ErrorTracking
+ class DetailedError
+ include ActiveModel::Model
+
+ attr_accessor :count,
+ :culprit,
+ :external_base_url,
+ :external_url,
+ :first_release_last_commit,
+ :first_release_short_version,
+ :first_seen,
+ :frequency,
+ :id,
+ :last_release_last_commit,
+ :last_release_short_version,
+ :last_seen,
+ :message,
+ :project_id,
+ :project_name,
+ :project_slug,
+ :short_id,
+ :status,
+ :title,
+ :type,
+ :user_count
+ end
+ end
+end
diff --git a/lib/gitlab/error_tracking/error_event.rb b/lib/gitlab/error_tracking/error_event.rb
new file mode 100644
index 00000000000..c6e0d82f868
--- /dev/null
+++ b/lib/gitlab/error_tracking/error_event.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ErrorTracking
+ class ErrorEvent
+ include ActiveModel::Model
+
+ attr_accessor :issue_id, :date_received, :stack_trace_entries
+ end
+ end
+end
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index 3d14a8dde8d..efddda0ec65 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -3,8 +3,6 @@
module Gitlab
module EtagCaching
class Router
- prepend_if_ee('EE::Gitlab::EtagCaching::Router') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
Route = Struct.new(:regexp, :name)
# We enable an ETag for every request matching the regex.
# To match a regex the path needs to match the following:
@@ -80,3 +78,5 @@ module Gitlab
end
end
end
+
+Gitlab::EtagCaching::Router.prepend_if_ee('EE::Gitlab::EtagCaching::Router')
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 895755376ee..948f720b01b 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -14,13 +14,15 @@ module Gitlab
signup_flow: {
feature_toggle: :experimental_separate_sign_up_flow,
environment: ::Gitlab.dev_env_or_com?,
- enabled_ratio: 0.1
+ enabled_ratio: 0.1,
+ tracking_category: 'Growth::Acquisition::Experiment::SignUpFlow'
}
}.freeze
# Controller concern that checks if an experimentation_subject_id cookie is present and sets it if absent.
# Used for A/B testing of experimental features. Exposes the `experiment_enabled?(experiment_name)` method
- # to controllers and views.
+ # to controllers and views. It returns true when the experiment is enabled and the user is selected as part
+ # of the experimental group.
#
module ControllerConcern
extend ActiveSupport::Concern
@@ -36,22 +38,67 @@ module Gitlab
cookies.permanent.signed[:experimentation_subject_id] = {
value: SecureRandom.uuid,
domain: :all,
- secure: ::Gitlab.config.gitlab.https
+ secure: ::Gitlab.config.gitlab.https,
+ httponly: true
}
end
def experiment_enabled?(experiment_key)
- Experimentation.enabled?(experiment_key, experimentation_subject_index)
+ Experimentation.enabled_for_user?(experiment_key, experimentation_subject_index) || forced_enabled?(experiment_key)
+ end
+
+ def track_experiment_event(experiment_key, action)
+ track_experiment_event_for(experiment_key, action) do |tracking_data|
+ ::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), tracking_data)
+ end
+ end
+
+ def frontend_experimentation_tracking_data(experiment_key, action)
+ track_experiment_event_for(experiment_key, action) do |tracking_data|
+ gon.push(tracking_data: tracking_data)
+ end
end
private
+ def experimentation_subject_id
+ cookies.signed[:experimentation_subject_id]
+ end
+
def experimentation_subject_index
- experimentation_subject_id = cookies.signed[:experimentation_subject_id]
return if experimentation_subject_id.blank?
experimentation_subject_id.delete('-').hex % 100
end
+
+ def track_experiment_event_for(experiment_key, action)
+ return unless Experimentation.enabled?(experiment_key)
+
+ yield experimentation_tracking_data(experiment_key, action)
+ end
+
+ def experimentation_tracking_data(experiment_key, action)
+ {
+ category: tracking_category(experiment_key),
+ action: action,
+ property: tracking_group(experiment_key),
+ label: experimentation_subject_id
+ }
+ end
+
+ def tracking_category(experiment_key)
+ Experimentation.experiment(experiment_key).tracking_category
+ end
+
+ def tracking_group(experiment_key)
+ return unless Experimentation.enabled?(experiment_key)
+
+ experiment_enabled?(experiment_key) ? 'experimental_group' : 'control_group'
+ end
+
+ def forced_enabled?(experiment_key)
+ params.has_key?(:force_experiment) && params[:force_experiment] == experiment_key.to_s
+ end
end
class << self
@@ -59,18 +106,20 @@ module Gitlab
Experiment.new(EXPERIMENTS[key].merge(key: key))
end
- def enabled?(experiment_key, experimentation_subject_index)
+ def enabled?(experiment_key)
return false unless EXPERIMENTS.key?(experiment_key)
experiment = experiment(experiment_key)
+ experiment.feature_toggle_enabled? && experiment.enabled_for_environment?
+ end
- experiment.feature_toggle_enabled? &&
- experiment.enabled_for_environment? &&
- experiment.enabled_for_experimentation_subject?(experimentation_subject_index)
+ def enabled_for_user?(experiment_key, experimentation_subject_index)
+ enabled?(experiment_key) &&
+ experiment(experiment_key).enabled_for_experimentation_subject?(experimentation_subject_index)
end
end
- Experiment = Struct.new(:key, :feature_toggle, :environment, :enabled_ratio, keyword_init: true) do
+ Experiment = Struct.new(:key, :feature_toggle, :environment, :enabled_ratio, :tracking_category, keyword_init: true) do
def feature_toggle_enabled?
return Feature.enabled?(key, default_enabled: true) if feature_toggle.nil?
diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb
index b5d308e462c..ce1370bab0f 100644
--- a/lib/gitlab/favicon.rb
+++ b/lib/gitlab/favicon.rb
@@ -7,7 +7,7 @@ module Gitlab
image_name =
if appearance.favicon.exists?
appearance.favicon_path
- elsif Gitlab::Utils.to_boolean(ENV['CANARY'])
+ elsif Gitlab.canary?
'favicon-yellow.png'
elsif Rails.env.development?
development_favicon
diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb
index 3958814208c..ec9d2df613b 100644
--- a/lib/gitlab/file_finder.rb
+++ b/lib/gitlab/file_finder.rb
@@ -15,12 +15,12 @@ module Gitlab
def find(query)
query = Gitlab::Search::Query.new(query, encode_binary: true) do
- filter :filename, matcher: ->(filter, blob) { blob.binary_filename =~ /#{filter[:regex_value]}$/i }
- filter :path, matcher: ->(filter, blob) { blob.binary_filename =~ /#{filter[:regex_value]}/i }
- filter :extension, matcher: ->(filter, blob) { blob.binary_filename =~ /\.#{filter[:regex_value]}$/i }
+ filter :filename, matcher: ->(filter, blob) { blob.binary_path =~ /#{filter[:regex_value]}$/i }
+ filter :path, matcher: ->(filter, blob) { blob.binary_path =~ /#{filter[:regex_value]}/i }
+ filter :extension, matcher: ->(filter, blob) { blob.binary_path =~ /\.#{filter[:regex_value]}$/i }
end
- files = find_by_filename(query.term) + find_by_content(query.term)
+ files = find_by_path(query.term) + find_by_content(query.term)
files = query.filter_results(files) if query.filters.any?
@@ -35,13 +35,14 @@ module Gitlab
end
end
- def find_by_filename(query)
- search_filenames(query).map do |filename|
- Gitlab::Search::FoundBlob.new(blob_filename: filename, project: project, ref: ref, repository: repository)
+ def find_by_path(query)
+ search_paths(query).map do |path|
+ Gitlab::Search::FoundBlob.new(blob_path: path, project: project, ref: ref, repository: repository)
end
end
- def search_filenames(query)
+ # Overriden in Gitlab::WikiFileFinder
+ def search_paths(query)
repository.search_files_by_name(query, ref)
end
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 8fac3621df9..6210223917b 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -155,10 +155,6 @@ module Gitlab
end
end
- def extract_signature(repository, commit_id)
- repository.gitaly_commit_client.extract_signature(commit_id)
- end
-
def extract_signature_lazily(repository, commit_id)
BatchLoader.for(commit_id).batch(key: repository) do |commit_ids, loader, args|
batch_signature_extraction(args[:key], commit_ids).each do |commit_id, signature_data|
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index b2c22898079..4971a18e270 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -25,9 +25,18 @@ module Gitlab
InvalidRef = Class.new(StandardError)
GitError = Class.new(StandardError)
DeleteBranchError = Class.new(StandardError)
- CreateTreeError = Class.new(StandardError)
TagExistsError = Class.new(StandardError)
ChecksumError = Class.new(StandardError)
+ class CreateTreeError < StandardError
+ attr_reader :error_code
+
+ def initialize(error_code)
+ super(self.class.name)
+
+ # The value coming from Gitaly is an uppercase String (e.g., "EMPTY")
+ @error_code = error_code.downcase.to_sym
+ end
+ end
# Directory name of repo
attr_reader :name
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index c1bcd8e934a..3025fc6bfdb 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -133,14 +133,6 @@ module Gitlab
GollumSlug.generate(title, format)
end
- def page_formatted_data(title:, dir: nil, version: nil)
- version = version&.id
-
- wrapped_gitaly_errors do
- gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version)
- end
- end
-
private
def gitaly_wiki_client
diff --git a/lib/gitlab/git_access_result/custom_action.rb b/lib/gitlab/git_access_result/custom_action.rb
index a05a4baed82..336f3405f72 100644
--- a/lib/gitlab/git_access_result/custom_action.rb
+++ b/lib/gitlab/git_access_result/custom_action.rb
@@ -3,7 +3,7 @@
module Gitlab
module GitAccessResult
class CustomAction
- attr_reader :payload, :message
+ attr_reader :payload, :console_messages
# Example of payload:
#
@@ -16,9 +16,9 @@ module Gitlab
# }
# }
#
- def initialize(payload, message)
+ def initialize(payload, console_messages)
@payload = payload
- @message = message
+ @console_messages = console_messages
end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index be695e7e91a..5b47853b9c1 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -142,18 +142,39 @@ module Gitlab
# kwargs.merge(deadline: Time.now + 10)
# end
#
- def self.call(storage, service, rpc, request, remote_storage: nil, timeout: default_timeout)
- start = Gitlab::Metrics::System.monotonic_time
- request_hash = request.is_a?(Google::Protobuf::MessageExts) ? request.to_h : {}
+ def self.call(storage, service, rpc, request, remote_storage: nil, timeout: default_timeout, &block)
+ self.measure_timings(service, rpc, request) do
+ self.execute(storage, service, rpc, request, remote_storage: remote_storage, timeout: timeout, &block)
+ end
+ end
+ # This method is like GitalyClient.call but should be used with
+ # Gitaly streaming RPCs. It measures how long the the RPC took to
+ # produce the full response, not just the initial response.
+ def self.streaming_call(storage, service, rpc, request, remote_storage: nil, timeout: default_timeout)
+ self.measure_timings(service, rpc, request) do
+ response = self.execute(storage, service, rpc, request, remote_storage: remote_storage, timeout: timeout)
+
+ yield(response)
+ end
+ end
+
+ def self.execute(storage, service, rpc, request, remote_storage:, timeout:)
enforce_gitaly_request_limits(:call)
kwargs = request_kwargs(storage, timeout: timeout.to_f, remote_storage: remote_storage)
kwargs = yield(kwargs) if block_given?
stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def self.measure_timings(service, rpc, request)
+ start = Gitlab::Metrics::System.monotonic_time
+
+ yield
ensure
duration = Gitlab::Metrics::System.monotonic_time - start
+ request_hash = request.is_a?(Google::Protobuf::MessageExts) ? request.to_h : {}
# Keep track, separately, for the performance bar
self.query_time += duration
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index b0559729ff3..15318bc817a 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -200,8 +200,9 @@ module Gitlab
to: to
)
- response = GitalyClient.call(@repository.storage, :commit_service, :commits_between, request, timeout: GitalyClient.medium_timeout)
- consume_commits_response(response)
+ GitalyClient.streaming_call(@repository.storage, :commit_service, :commits_between, request, timeout: GitalyClient.medium_timeout) do |response|
+ consume_commits_response(response)
+ end
end
def diff_stats(left_commit_sha, right_commit_sha)
@@ -224,8 +225,9 @@ module Gitlab
)
request.order = opts[:order].upcase if opts[:order].present?
- response = GitalyClient.call(@repository.storage, :commit_service, :find_all_commits, request, timeout: GitalyClient.medium_timeout)
- consume_commits_response(response)
+ GitalyClient.streaming_call(@repository.storage, :commit_service, :find_all_commits, request, timeout: GitalyClient.medium_timeout) do |response|
+ consume_commits_response(response)
+ end
end
def list_commits_by_oid(oids)
@@ -233,8 +235,9 @@ module Gitlab
request = Gitaly::ListCommitsByOidRequest.new(repository: @gitaly_repo, oid: oids)
- response = GitalyClient.call(@repository.storage, :commit_service, :list_commits_by_oid, request, timeout: GitalyClient.medium_timeout)
- consume_commits_response(response)
+ GitalyClient.streaming_call(@repository.storage, :commit_service, :list_commits_by_oid, request, timeout: GitalyClient.medium_timeout) do |response|
+ consume_commits_response(response)
+ end
rescue GRPC::NotFound # If no repository is found, happens mainly during testing
[]
end
@@ -249,8 +252,9 @@ module Gitlab
offset: offset.to_i
)
- response = GitalyClient.call(@repository.storage, :commit_service, :commits_by_message, request, timeout: GitalyClient.medium_timeout)
- consume_commits_response(response)
+ GitalyClient.streaming_call(@repository.storage, :commit_service, :commits_by_message, request, timeout: GitalyClient.medium_timeout) do |response|
+ consume_commits_response(response)
+ end
end
def languages(ref = nil)
@@ -323,9 +327,9 @@ module Gitlab
request.paths = encode_repeated(Array(options[:path])) if options[:path].present?
- response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request, timeout: GitalyClient.medium_timeout)
-
- consume_commits_response(response)
+ GitalyClient.streaming_call(@repository.storage, :commit_service, :find_commits, request, timeout: GitalyClient.medium_timeout) do |response|
+ consume_commits_response(response)
+ end
end
def filter_shas_with_signatures(shas)
@@ -348,25 +352,6 @@ module Gitlab
end
end
- def extract_signature(commit_id)
- request = Gitaly::ExtractCommitSignatureRequest.new(repository: @gitaly_repo, commit_id: commit_id)
- response = GitalyClient.call(@repository.storage, :commit_service, :extract_commit_signature, request, timeout: GitalyClient.fast_timeout)
-
- signature = +''.b
- signed_text = +''.b
-
- response.each do |message|
- signature << message.signature
- signed_text << message.signed_text
- end
-
- return if signature.blank? && signed_text.blank?
-
- [signature, signed_text]
- rescue GRPC::InvalidArgument => ex
- raise ArgumentError, ex
- 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, timeout: GitalyClient.fast_timeout)
diff --git a/lib/gitlab/gitaly_client/namespace_service.rb b/lib/gitlab/gitaly_client/namespace_service.rb
index 0be214f3035..dbcebec3aa2 100644
--- a/lib/gitlab/gitaly_client/namespace_service.rb
+++ b/lib/gitlab/gitaly_client/namespace_service.rb
@@ -3,14 +3,23 @@
module Gitlab
module GitalyClient
class NamespaceService
- def initialize(storage)
- @storage = storage
+ extend Gitlab::TemporarilyAllow
+
+ NamespaceServiceAccessError = Class.new(StandardError)
+ ALLOW_KEY = :allow_namespace
+
+ def self.allow
+ temporarily_allow(ALLOW_KEY) { yield }
end
- def exists?(name)
- request = Gitaly::NamespaceExistsRequest.new(storage_name: @storage, name: name)
+ def self.denied?
+ !temporarily_allowed?(ALLOW_KEY)
+ end
+
+ def initialize(storage)
+ raise NamespaceServiceAccessError if self.class.denied?
- gitaly_client_call(:namespace_exists, request, timeout: GitalyClient.fast_timeout).exists
+ @storage = storage
end
def add(name)
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 6e486c763da..61c5db4c4df 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -447,7 +447,7 @@ module Gitlab
elsif response.commit_error.presence
raise Gitlab::Git::CommitError, response.commit_error
elsif response.create_tree_error.presence
- raise Gitlab::Git::Repository::CreateTreeError, response.create_tree_error
+ raise Gitlab::Git::Repository::CreateTreeError, response.create_tree_error_code
end
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 15e0d7349dd..9034edb6263 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -179,18 +179,6 @@ module Gitlab
wiki_file
end
- def get_formatted_data(title:, dir: nil, version: nil)
- request = Gitaly::WikiGetFormattedDataRequest.new(
- repository: @gitaly_repo,
- title: encode_binary(title),
- revision: encode_binary(version),
- directory: encode_binary(dir)
- )
-
- response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_formatted_data, request, timeout: GitalyClient.medium_timeout)
- response.reduce([]) { |memo, msg| memo << msg.data }.join
- end
-
private
# If a block is given and the yielded value is truthy, iteration will be
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index f1e31a615a4..2616a19fdaa 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -42,9 +42,6 @@ module Gitlab
# Initialize gon.features with any flags that should be
# made globally available to the frontend
push_frontend_feature_flag(:suppress_ajax_navigation_errors, default_enabled: true)
-
- # Flag controls a GFM feature used across many routes.
- push_frontend_feature_flag(:gfm_grafana_integration)
end
# Exposes the state of a feature flag to the frontend code.
diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb
index 32f61b1d65c..1dce26efc65 100644
--- a/lib/gitlab/gpg.rb
+++ b/lib/gitlab/gpg.rb
@@ -4,6 +4,10 @@ module Gitlab
module Gpg
extend self
+ CleanupError = Class.new(StandardError)
+ BG_CLEANUP_RUNTIME_S = 2
+ FG_CLEANUP_RUNTIME_S = 0.5
+
MUTEX = Mutex.new
module CurrentKeyChain
@@ -94,16 +98,55 @@ module Gitlab
previous_dir = current_home_dir
tmp_dir = Dir.mktmpdir
GPGME::Engine.home_dir = tmp_dir
+ tmp_keychains_created.increment
+
yield
ensure
- # Ignore any errors when removing the tmp directory, as we may run into a
+ GPGME::Engine.home_dir = previous_dir
+
+ begin
+ cleanup_tmp_dir(tmp_dir)
+ rescue CleanupError => e
+ # This means we left a GPG-agent process hanging. Logging the problem in
+ # sentry will make this more visible.
+ Gitlab::Sentry.track_exception(e,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab/issues/20918',
+ extra: { tmp_dir: tmp_dir })
+ end
+
+ tmp_keychains_removed.increment unless File.exist?(tmp_dir)
+ end
+
+ def cleanup_tmp_dir(tmp_dir)
+ return FileUtils.remove_entry(tmp_dir, true) if Feature.disabled?(:gpg_cleanup_retries)
+
+ # Retry when removing the tmp directory failed, as we may run into a
# race condition:
# The `gpg-agent` agent process may clean up some files as well while
# `FileUtils.remove_entry` is iterating the directory and removing all
# its contained files and directories recursively, which could raise an
# error.
- FileUtils.remove_entry(tmp_dir, true)
- GPGME::Engine.home_dir = previous_dir
+ # Failing to remove the tmp directory could leave the `gpg-agent` process
+ # running forever.
+ Retriable.retriable(max_elapsed_time: cleanup_time, base_interval: 0.1) do
+ FileUtils.remove_entry(tmp_dir) if File.exist?(tmp_dir)
+ end
+ rescue => e
+ raise CleanupError, e
+ end
+
+ def cleanup_time
+ Sidekiq.server? ? BG_CLEANUP_RUNTIME_S : FG_CLEANUP_RUNTIME_S
+ end
+
+ def tmp_keychains_created
+ @tmp_keychains_created ||= Gitlab::Metrics.counter(:gpg_tmp_keychains_created_total,
+ 'The number of temporary GPG keychains created')
+ end
+
+ def tmp_keychains_removed
+ @tmp_keychains_removed ||= Gitlab::Metrics.counter(:gpg_tmp_keychains_removed_total,
+ 'The number of temporary GPG keychains removed')
end
end
end
diff --git a/lib/gitlab/grape_logging/loggers/exception_logger.rb b/lib/gitlab/grape_logging/loggers/exception_logger.rb
new file mode 100644
index 00000000000..022eb15d28d
--- /dev/null
+++ b/lib/gitlab/grape_logging/loggers/exception_logger.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GrapeLogging
+ module Loggers
+ class ExceptionLogger < ::GrapeLogging::Loggers::Base
+ def parameters(request, _)
+ # grape-logging attempts to pass the logger the exception
+ # (https://github.com/aserafin/grape_logging/blob/v1.7.0/lib/grape_logging/middleware/request_logger.rb#L63),
+ # but it appears that the rescue_all in api.rb takes
+ # precedence so the logger never sees it. We need to
+ # store and retrieve the exception from the environment.
+ exception = request.env[::API::Helpers::API_EXCEPTION_ENV]
+
+ return {} unless exception.is_a?(Exception)
+
+ data = {
+ exception: {
+ class: exception.class.to_s,
+ message: exception.message
+ }
+ }
+
+ if exception.backtrace
+ data[:exception][:backtrace] = Gitlab::Profiler.clean_backtrace(exception.backtrace)
+ end
+
+ data
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb
index 15ecc3b04f0..f9ff2b30eae 100644
--- a/lib/gitlab/graphql/authorize/instrumentation.rb
+++ b/lib/gitlab/graphql/authorize/instrumentation.rb
@@ -9,12 +9,16 @@ module Gitlab
def instrument(_type, field)
service = AuthorizeFieldService.new(field)
- if service.authorizations?
+ if service.authorizations? && !resolver_skips_authorizations?(field)
field.redefine { resolve(service.authorized_resolve) }
else
field
end
end
+
+ def resolver_skips_authorizations?(field)
+ field.metadata[:resolver].try(:skip_authorizations?)
+ end
end
end
end
diff --git a/lib/gitlab/graphql/connections.rb b/lib/gitlab/graphql/connections.rb
index fbccdfa7b08..38c7d98f37c 100644
--- a/lib/gitlab/graphql/connections.rb
+++ b/lib/gitlab/graphql/connections.rb
@@ -6,7 +6,11 @@ module Gitlab
def self.use(_schema)
GraphQL::Relay::BaseConnection.register_connection_implementation(
ActiveRecord::Relation,
- Gitlab::Graphql::Connections::KeysetConnection
+ Gitlab::Graphql::Connections::Keyset::Connection
+ )
+ GraphQL::Relay::BaseConnection.register_connection_implementation(
+ Gitlab::Graphql::FilterableArray,
+ Gitlab::Graphql::Connections::FilterableArrayConnection
)
end
end
diff --git a/lib/gitlab/graphql/connections/filterable_array_connection.rb b/lib/gitlab/graphql/connections/filterable_array_connection.rb
new file mode 100644
index 00000000000..800f2c949c6
--- /dev/null
+++ b/lib/gitlab/graphql/connections/filterable_array_connection.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ # FilterableArrayConnection is useful especially for lazy-loaded values.
+ # It allows us to call a callback only on the slice of array being
+ # rendered in the "after loaded" phase. For example we can check
+ # permissions only on a small subset of items.
+ class FilterableArrayConnection < GraphQL::Relay::ArrayConnection
+ def paged_nodes
+ @filtered_nodes ||= nodes.filter_callback.call(super)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb
new file mode 100644
index 00000000000..22728cc0b65
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ module Conditions
+ class BaseCondition
+ def initialize(arel_table, names, values, operator, before_or_after)
+ @arel_table, @names, @values, @operator, @before_or_after = arel_table, names, values, operator, before_or_after
+ end
+
+ def build
+ raise NotImplementedError
+ end
+
+ private
+
+ attr_reader :arel_table, :names, :values, :operator, :before_or_after
+
+ def table_condition(attribute, value, operator)
+ case operator
+ when '>'
+ arel_table[attribute].gt(value)
+ when '<'
+ arel_table[attribute].lt(value)
+ when '='
+ arel_table[attribute].eq(value)
+ when 'is_null'
+ arel_table[attribute].eq(nil)
+ when 'is_not_null'
+ arel_table[attribute].not_eq(nil)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb
new file mode 100644
index 00000000000..3b56ddb996d
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ module Conditions
+ class NotNullCondition < BaseCondition
+ def build
+ conditions = [first_attribute_condition]
+
+ # If there is only one order field, we can assume it
+ # does not contain NULLs, and don't need additional
+ # conditions
+ unless names.count == 1
+ conditions << [second_attribute_condition, final_condition]
+ end
+
+ conditions.join
+ end
+
+ private
+
+ # ex: "(relative_position > 23)"
+ def first_attribute_condition
+ <<~SQL
+ (#{table_condition(names.first, values.first, operator.first).to_sql})
+ SQL
+ end
+
+ # ex: " OR (relative_position = 23 AND id > 500)"
+ def second_attribute_condition
+ condition = <<~SQL
+ OR (
+ #{table_condition(names.first, values.first, '=').to_sql}
+ AND
+ #{table_condition(names[1], values[1], operator[1]).to_sql}
+ )
+ SQL
+
+ condition
+ end
+
+ # ex: " OR (relative_position IS NULL)"
+ def final_condition
+ if before_or_after == :after
+ <<~SQL
+ OR (#{table_condition(names.first, nil, 'is_null').to_sql})
+ SQL
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb
new file mode 100644
index 00000000000..71a74936d5d
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ module Conditions
+ class NullCondition < BaseCondition
+ def build
+ [first_attribute_condition, final_condition].join
+ end
+
+ private
+
+ # ex: "(relative_position IS NULL AND id > 500)"
+ def first_attribute_condition
+ condition = <<~SQL
+ (
+ #{table_condition(names.first, nil, 'is_null').to_sql}
+ AND
+ #{table_condition(names[1], values[1], operator[1]).to_sql}
+ )
+ SQL
+
+ condition
+ end
+
+ # ex: " OR (relative_position IS NOT NULL)"
+ def final_condition
+ if before_or_after == :before
+ <<~SQL
+ OR (#{table_condition(names.first, nil, 'is_not_null').to_sql})
+ SQL
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/connection.rb b/lib/gitlab/graphql/connections/keyset/connection.rb
new file mode 100644
index 00000000000..c75ea206edb
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/connection.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+# Keyset::Connection provides cursor based pagination, to avoid using OFFSET.
+# It basically sorts / filters using WHERE sorting_value > cursor.
+# We do this for performance reasons (https://gitlab.com/gitlab-org/gitlab-foss/issues/45756),
+# as well as for having stable pagination
+# https://graphql-ruby.org/pro/cursors.html#whats-the-difference
+# https://coderwall.com/p/lkcaag/pagination-you-re-probably-doing-it-wrong
+#
+# It currently supports sorting on two columns, but the last column must
+# be the primary key. If it's not already included, an order on the
+# primary key will be added automatically, like `order(id: :desc)`
+#
+# Issue.order(created_at: :asc).order(:id)
+# Issue.order(due_date: :asc)
+#
+# You can also use `Gitlab::Database.nulls_last_order`:
+#
+# Issue.reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC'))
+#
+# It will tolerate non-attribute ordering, but only attributes determine the cursor.
+# For example, this is legitimate:
+#
+# Issue.order('issues.due_date IS NULL').order(due_date: :asc).order(:id)
+#
+# but anything more complex has a chance of not working.
+#
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ class Connection < GraphQL::Relay::BaseConnection
+ include Gitlab::Utils::StrongMemoize
+
+ # TODO https://gitlab.com/gitlab-org/gitlab/issues/35104
+ include Gitlab::Graphql::Connections::Keyset::LegacyKeysetConnection
+
+ def cursor_from_node(node)
+ return legacy_cursor_from_node(node) if use_legacy_pagination?
+
+ encoded_json_from_ordering(node)
+ end
+
+ def sliced_nodes
+ return legacy_sliced_nodes if use_legacy_pagination?
+
+ @sliced_nodes ||=
+ begin
+ OrderInfo.validate_ordering(ordered_nodes, order_list)
+
+ sliced = ordered_nodes
+ sliced = slice_nodes(sliced, before, :before) if before.present?
+ sliced = slice_nodes(sliced, after, :after) if after.present?
+
+ sliced
+ end
+ end
+
+ def paged_nodes
+ # These are the nodes that will be loaded into memory for rendering
+ # So we're ok loading them into memory here as that's bound to happen
+ # anyway. Having them ready means we can modify the result while
+ # rendering the fields.
+ @paged_nodes ||= load_paged_nodes.to_a
+ end
+
+ private
+
+ def load_paged_nodes
+ if first && last
+ raise Gitlab::Graphql::Errors::ArgumentError.new("Can only provide either `first` or `last`, not both")
+ end
+
+ if last
+ sliced_nodes.last(limit_value)
+ else
+ sliced_nodes.limit(limit_value) # rubocop: disable CodeReuse/ActiveRecord
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def slice_nodes(sliced, encoded_cursor, before_or_after)
+ decoded_cursor = ordering_from_encoded_json(encoded_cursor)
+ builder = QueryBuilder.new(arel_table, order_list, decoded_cursor, before_or_after)
+ ordering = builder.conditions
+
+ sliced.where(*ordering).where.not(id: decoded_cursor['id'])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def limit_value
+ @limit_value ||= [first, last, max_page_size].compact.min
+ end
+
+ def ordered_nodes
+ strong_memoize(:order_nodes) do
+ unless nodes.primary_key.present?
+ raise ArgumentError.new('Relation must have a primary key')
+ end
+
+ list = OrderInfo.build_order_list(nodes)
+
+ # ensure there is a primary key ordering
+ if list&.last&.attribute_name != nodes.primary_key
+ nodes.order(arel_table[nodes.primary_key].desc) # rubocop: disable CodeReuse/ActiveRecord
+ else
+ nodes
+ end
+ end
+ end
+
+ def order_list
+ strong_memoize(:order_list) do
+ OrderInfo.build_order_list(ordered_nodes)
+ end
+ end
+
+ def arel_table
+ nodes.arel_table
+ end
+
+ # Storing the current order values in the cursor allows us to
+ # make an intelligent decision on handling NULL values.
+ # Otherwise we would either need to fetch the record first,
+ # or fetch it in the SQL, significantly complicating it.
+ def encoded_json_from_ordering(node)
+ ordering = { 'id' => node[:id].to_s }
+
+ order_list.each do |field|
+ field_name = field.attribute_name
+ ordering[field_name] = node[field_name].to_s
+ end
+
+ encode(ordering.to_json)
+ end
+
+ def ordering_from_encoded_json(cursor)
+ JSON.parse(decode(cursor))
+ rescue JSON::ParserError
+ # for the transition period where a client might request using an
+ # old style cursor. Once removed, make it an error:
+ # raise Gitlab::Graphql::Errors::ArgumentError, "Please provide a valid cursor"
+ # TODO can be removed in next release
+ # https://gitlab.com/gitlab-org/gitlab/issues/32933
+ field_name = order_list.first.attribute_name
+
+ { field_name => decode(cursor) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection.rb b/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection.rb
new file mode 100644
index 00000000000..baf900d1048
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+# TODO https://gitlab.com/gitlab-org/gitlab/issues/35104
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ module LegacyKeysetConnection
+ def legacy_cursor_from_node(node)
+ encode(node[legacy_order_field].to_s)
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def legacy_sliced_nodes
+ @sliced_nodes ||=
+ begin
+ sliced = nodes
+
+ sliced = sliced.where(legacy_before_slice) if before.present?
+ sliced = sliced.where(legacy_after_slice) if after.present?
+
+ sliced
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def use_legacy_pagination?
+ strong_memoize(:feature_disabled) do
+ Feature.disabled?(:graphql_keyset_pagination, default_enabled: true)
+ end
+ end
+
+ def legacy_before_slice
+ if legacy_sort_direction == :asc
+ arel_table[legacy_order_field].lt(decode(before))
+ else
+ arel_table[legacy_order_field].gt(decode(before))
+ end
+ end
+
+ def legacy_after_slice
+ if legacy_sort_direction == :asc
+ arel_table[legacy_order_field].gt(decode(after))
+ else
+ arel_table[legacy_order_field].lt(decode(after))
+ end
+ end
+
+ def legacy_order_info
+ @legacy_order_info ||= nodes.order_values.first
+ end
+
+ def legacy_order_field
+ @legacy_order_field ||= legacy_order_info&.expr&.name || nodes.primary_key
+ end
+
+ def legacy_sort_direction
+ @legacy_order_direction ||= legacy_order_info&.direction || :desc
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/order_info.rb b/lib/gitlab/graphql/connections/keyset/order_info.rb
new file mode 100644
index 00000000000..4d85e8f79b7
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/order_info.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ class OrderInfo
+ attr_reader :attribute_name, :sort_direction
+
+ def initialize(order_value)
+ if order_value.is_a?(String)
+ @attribute_name, @sort_direction = extract_nulls_last_order(order_value)
+ else
+ @attribute_name = order_value.expr.name
+ @sort_direction = order_value.direction
+ end
+ end
+
+ def operator_for(before_or_after)
+ case before_or_after
+ when :before
+ sort_direction == :asc ? '<' : '>'
+ when :after
+ sort_direction == :asc ? '>' : '<'
+ end
+ end
+
+ # Only allow specific node types
+ def self.build_order_list(relation)
+ order_list = relation.order_values.select do |value|
+ supported_order_value?(value)
+ end
+
+ order_list.map { |info| OrderInfo.new(info) }
+ end
+
+ def self.validate_ordering(relation, order_list)
+ if order_list.empty?
+ raise ArgumentError.new('A minimum of 1 ordering field is required')
+ end
+
+ if order_list.count > 2
+ raise ArgumentError.new('A maximum of 2 ordering fields are allowed')
+ end
+
+ # make sure the last ordering field is non-nullable
+ attribute_name = order_list.last&.attribute_name
+
+ if relation.columns_hash[attribute_name].null
+ raise ArgumentError.new("Column `#{attribute_name}` must not allow NULL")
+ end
+
+ if order_list.last.attribute_name != relation.primary_key
+ raise ArgumentError.new("Last ordering field must be the primary key, `#{relation.primary_key}`")
+ end
+ end
+
+ def self.supported_order_value?(order_value)
+ return true if order_value.is_a?(Arel::Nodes::Ascending) || order_value.is_a?(Arel::Nodes::Descending)
+ return false unless order_value.is_a?(String)
+
+ tokens = order_value.downcase.split
+
+ tokens.last(2) == %w(nulls last) && tokens.count == 4
+ end
+
+ private
+
+ def extract_nulls_last_order(order_value)
+ tokens = order_value.downcase.split
+
+ [tokens.first, (tokens[1] == 'asc' ? :asc : :desc)]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/query_builder.rb b/lib/gitlab/graphql/connections/keyset/query_builder.rb
new file mode 100644
index 00000000000..e93c25d85fc
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/query_builder.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ class QueryBuilder
+ def initialize(arel_table, order_list, decoded_cursor, before_or_after)
+ @arel_table, @order_list, @decoded_cursor, @before_or_after = arel_table, order_list, decoded_cursor, before_or_after
+
+ if order_list.empty?
+ raise ArgumentError.new('No ordering scopes have been supplied')
+ end
+ end
+
+ # Based on whether the main field we're ordering on is NULL in the
+ # cursor, we can more easily target our query condition.
+ # We assume that the last ordering field is unique, meaning
+ # it will not contain NULLs.
+ # We currently only support two ordering fields.
+ #
+ # Example of the conditions for
+ # relation: Issue.order(relative_position: :asc).order(id: :asc)
+ # after cursor: relative_position: 1500, id: 500
+ #
+ # when cursor[relative_position] is not NULL
+ #
+ # ("issues"."relative_position" > 1500)
+ # OR (
+ # "issues"."relative_position" = 1500
+ # AND
+ # "issues"."id" > 500
+ # )
+ # OR ("issues"."relative_position" IS NULL)
+ #
+ # when cursor[relative_position] is NULL
+ #
+ # "issues"."relative_position" IS NULL
+ # AND
+ # "issues"."id" > 500
+ #
+ def conditions
+ attr_names = order_list.map { |field| field.attribute_name }
+ attr_values = attr_names.map { |name| decoded_cursor[name] }
+
+ if attr_names.count == 1 && attr_values.first.nil?
+ raise Gitlab::Graphql::Errors::ArgumentError.new('Before/after cursor invalid: `nil` was provided as only sortable value')
+ end
+
+ if attr_names.count == 1 || attr_values.first.present?
+ Keyset::Conditions::NotNullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build
+ else
+ Keyset::Conditions::NullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build
+ end
+ end
+
+ private
+
+ attr_reader :arel_table, :order_list, :decoded_cursor, :before_or_after
+
+ def operators
+ order_list.map { |field| field.operator_for(before_or_after) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset_connection.rb b/lib/gitlab/graphql/connections/keyset_connection.rb
deleted file mode 100644
index 715963a44c1..00000000000
--- a/lib/gitlab/graphql/connections/keyset_connection.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Connections
- class KeysetConnection < GraphQL::Relay::BaseConnection
- def cursor_from_node(node)
- encode(node[order_field].to_s)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def sliced_nodes
- @sliced_nodes ||=
- begin
- sliced = nodes
-
- sliced = sliced.where(before_slice) if before.present?
- sliced = sliced.where(after_slice) if after.present?
-
- sliced
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def paged_nodes
- # These are the nodes that will be loaded into memory for rendering
- # So we're ok loading them into memory here as that's bound to happen
- # anyway. Having them ready means we can modify the result while
- # rendering the fields.
- @paged_nodes ||= load_paged_nodes.to_a
- end
-
- private
-
- def load_paged_nodes
- if first && last
- raise Gitlab::Graphql::Errors::ArgumentError.new("Can only provide either `first` or `last`, not both")
- end
-
- if last
- sliced_nodes.last(limit_value)
- else
- sliced_nodes.limit(limit_value) # rubocop: disable CodeReuse/ActiveRecord
- end
- end
-
- def before_slice
- if sort_direction == :asc
- table[order_field].lt(decode(before))
- else
- table[order_field].gt(decode(before))
- end
- end
-
- def after_slice
- if sort_direction == :asc
- table[order_field].gt(decode(after))
- else
- table[order_field].lt(decode(after))
- end
- end
-
- def limit_value
- @limit_value ||= [first, last, max_page_size].compact.min
- end
-
- def table
- nodes.arel_table
- end
-
- def order_info
- @order_info ||= nodes.order_values.first
- end
-
- def order_field
- @order_field ||= order_info&.expr&.name || nodes.primary_key
- end
-
- def sort_direction
- @order_direction ||= order_info&.direction || :desc
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/filterable_array.rb b/lib/gitlab/graphql/filterable_array.rb
new file mode 100644
index 00000000000..4909d291fd6
--- /dev/null
+++ b/lib/gitlab/graphql/filterable_array.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ class FilterableArray < Array
+ attr_reader :filter_callback
+
+ def initialize(filter_callback, *args)
+ super(args)
+ @filter_callback = filter_callback
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/loaders/pipeline_for_sha_loader.rb b/lib/gitlab/graphql/loaders/pipeline_for_sha_loader.rb
deleted file mode 100644
index 70344392138..00000000000
--- a/lib/gitlab/graphql/loaders/pipeline_for_sha_loader.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Loaders
- class PipelineForShaLoader
- attr_accessor :project, :sha
-
- def initialize(project, sha)
- @project, @sha = project, sha
- end
-
- def find_last
- BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args|
- pipelines = args[:key].ci_pipelines.latest_for_shas(shas)
-
- pipelines.each do |pipeline|
- loader.call(pipeline.sha, pipeline)
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/health_checks/master_check.rb b/lib/gitlab/health_checks/master_check.rb
new file mode 100644
index 00000000000..057bce84ddd
--- /dev/null
+++ b/lib/gitlab/health_checks/master_check.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module HealthChecks
+ # This check is registered on master,
+ # and validated by worker
+ class MasterCheck
+ extend SimpleAbstractCheck
+
+ class << self
+ def register_master
+ # when we fork, we pass the read pipe to child
+ # child can then react on whether the other end
+ # of pipe is still available
+ @pipe_read, @pipe_write = IO.pipe
+ end
+
+ def finish_master
+ close_read
+ close_write
+ end
+
+ def register_worker
+ # fork needs to close the pipe
+ close_write
+ end
+
+ private
+
+ def close_read
+ @pipe_read&.close
+ @pipe_read = nil
+ end
+
+ def close_write
+ @pipe_write&.close
+ @pipe_write = nil
+ end
+
+ def metric_prefix
+ 'master_check'
+ end
+
+ def successful?(result)
+ result
+ end
+
+ def check
+ # the lack of pipe is a legitimate failure of check
+ return false unless @pipe_read
+
+ @pipe_read.read_nonblock(1)
+
+ true
+ rescue IO::EAGAINWaitReadable
+ # if it is blocked, it means that the pipe is still open
+ # and there's no data waiting on it
+ true
+ rescue EOFError
+ # the pipe is closed
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index b2ac60fe825..516e7f54a6e 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def storage_path
- File.join(Settings.shared['path'], 'tmp/project_exports')
+ File.join(Settings.shared['path'], 'tmp/gitlab_exports')
end
def import_upload_path(filename:)
@@ -50,8 +50,8 @@ module Gitlab
'VERSION'
end
- def export_filename(project:)
- basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.full_path.tr('/', '_')}"
+ def export_filename(exportable:)
+ basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{exportable.full_path.tr('/', '_')}"
"#{basename[0..FILENAME_LIMIT]}_export.tar.gz"
end
@@ -63,6 +63,14 @@ module Gitlab
def reset_tokens?
true
end
+
+ def group_filename
+ 'group.json'
+ end
+
+ def group_config_file
+ Rails.root.join('lib/gitlab/import_export/group_import_export.yml')
+ end
end
end
diff --git a/lib/gitlab/import_export/config.rb b/lib/gitlab/import_export/config.rb
index 6f4919ead4e..83c4bc47349 100644
--- a/lib/gitlab/import_export/config.rb
+++ b/lib/gitlab/import_export/config.rb
@@ -3,7 +3,8 @@
module Gitlab
module ImportExport
class Config
- def initialize
+ def initialize(config: Gitlab::ImportExport.config_file)
+ @config = config
@hash = parse_yaml
@hash.deep_symbolize_keys!
@ee_hash = @hash.delete(:ee) || {}
@@ -50,7 +51,7 @@ module Gitlab
end
def parse_yaml
- YAML.load_file(Gitlab::ImportExport.config_file)
+ YAML.load_file(@config)
end
end
end
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index 05432f433e7..2fd12e3aa78 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -60,7 +60,7 @@ module Gitlab
def copy_archive
return if @archive_file
- @archive_file = File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(project: @project))
+ @archive_file = File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(exportable: @project))
download_or_copy_upload(@project.import_export_upload.import_file, @archive_file)
end
diff --git a/lib/gitlab/import_export/group_import_export.yml b/lib/gitlab/import_export/group_import_export.yml
new file mode 100644
index 00000000000..c1900350c86
--- /dev/null
+++ b/lib/gitlab/import_export/group_import_export.yml
@@ -0,0 +1,36 @@
+# Model relationships to be included in the group import/export
+#
+# This list _must_ only contain relationships that are available to both FOSS and
+# Enterprise editions. EE specific relationships must be defined in the `ee` section further
+# down below.
+tree:
+ group:
+ - :milestones
+ - :badges
+ - labels:
+ - :priorities
+ - :boards
+ - members:
+ - :user
+
+included_attributes:
+
+excluded_attributes:
+ group:
+ - :runners_token
+ - :runners_token_encrypted
+
+methods:
+ labels:
+ - :type
+ badges:
+ - :type
+
+preloads:
+
+# EE specific relationships and settings to include. All of this will be merged
+# into the previous structures if EE is used.
+ee:
+ tree:
+ group:
+ - :epics
diff --git a/lib/gitlab/import_export/group_project_object_builder.rb b/lib/gitlab/import_export/group_project_object_builder.rb
index de1629d0e28..b94839363df 100644
--- a/lib/gitlab/import_export/group_project_object_builder.rb
+++ b/lib/gitlab/import_export/group_project_object_builder.rb
@@ -49,11 +49,12 @@ module Gitlab
].compact
end
- # Returns Arel clause `"{table_name}"."project_id" = {project.id}`
+ # Returns Arel clause `"{table_name}"."project_id" = {project.id}` if project is present
+ # For example: merge_request has :target_project_id, and we are searching by :iid
# or, if group is present:
# `"{table_name}"."project_id" = {project.id} OR "{table_name}"."group_id" = {group.id}`
def where_clause_base
- clause = table[:project_id].eq(project.id)
+ clause = table[:project_id].eq(project.id) if project
clause = clause.or(table[:group_id].eq(group.id)) if group
clause
@@ -103,6 +104,10 @@ module Gitlab
klass == Milestone
end
+ def merge_request?
+ klass == MergeRequest
+ end
+
# If an existing group milestone used the IID
# claim the IID back and set the group milestone to use one available
# This is necessary to fix situations like the following:
@@ -124,7 +129,7 @@ module Gitlab
# Returns Arel clause for a particular model or `nil`.
def where_clause_for_klass
- # no-op
+ return attrs_to_arel(attributes.slice('iid')) if merge_request?
end
end
end
diff --git a/lib/gitlab/import_export/group_tree_saver.rb b/lib/gitlab/import_export/group_tree_saver.rb
new file mode 100644
index 00000000000..8d2fb881cc0
--- /dev/null
+++ b/lib/gitlab/import_export/group_tree_saver.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ class GroupTreeSaver
+ attr_reader :full_path
+
+ def initialize(group:, current_user:, shared:, params: {})
+ @params = params
+ @current_user = current_user
+ @shared = shared
+ @group = group
+ @full_path = File.join(@shared.export_path, ImportExport.group_filename)
+ end
+
+ def save
+ group_tree = serialize(@group, reader.group_tree)
+ tree_saver.save(group_tree, @shared.export_path, ImportExport.group_filename)
+
+ true
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def serialize(group, relations_tree)
+ group_tree = tree_saver.serialize(group, relations_tree)
+
+ group.children.each do |child|
+ group_tree['children'] ||= []
+ group_tree['children'] << serialize(child, relations_tree)
+ end
+
+ group_tree
+ rescue => e
+ @shared.error(e)
+ end
+
+ def reader
+ @reader ||= Gitlab::ImportExport::Reader.new(
+ shared: @shared,
+ config: Gitlab::ImportExport::Config.new(
+ config: Gitlab::ImportExport.group_config_file
+ ).to_h
+ )
+ end
+
+ def tree_saver
+ @tree_saver ||= RelationTreeSaver.new
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 141e73e6a47..1aafe5804c0 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -28,6 +28,7 @@ tree:
- label:
- :priorities
- :issue_assignees
+ - :zoom_meetings
- snippets:
- :award_emoji
- notes:
@@ -147,6 +148,8 @@ excluded_attributes:
- :emails_disabled
- :max_pages_size
- :max_artifacts_size
+ - :marked_for_deletion_at
+ - :marked_for_deletion_by_user_id
namespaces:
- :runners_token
- :runners_token_encrypted
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 3fa5765fd4a..c401f96b5c1 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -15,7 +15,6 @@ module Gitlab
@user = user
@shared = shared
@project = project
- @saved = true
end
def restore
@@ -33,7 +32,8 @@ module Gitlab
ActiveRecord::Base.uncached do
ActiveRecord::Base.no_touching do
update_project_params!
- create_relations
+ create_project_relations!
+ post_import!
end
end
@@ -69,81 +69,75 @@ module Gitlab
# in the DB. The structure and relationships between models are guessed from
# the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project.
- def create_relations
- project_relations.each do |relation_key, relation_definition|
- relation_key_s = relation_key.to_s
-
- if relation_definition.present?
- create_sub_relations(relation_key_s, relation_definition, @tree_hash)
- elsif @tree_hash[relation_key_s].present?
- save_relation_hash(relation_key_s, @tree_hash[relation_key_s])
- end
- end
+ def create_project_relations!
+ project_relations.each(&method(
+ :process_project_relation!))
+ end
+ def post_import!
@project.merge_requests.set_latest_merge_request_diff_ids!
-
- @saved
end
- def save_relation_hash(relation_key, relation_hash_batch)
- relation_hash = create_relation(relation_key, relation_hash_batch)
+ def process_project_relation!(relation_key, relation_definition)
+ data_hashes = @tree_hash.delete(relation_key)
+ return unless data_hashes
- remove_group_models(relation_hash) if relation_hash.is_a?(Array)
+ # we do not care if we process array or hash
+ data_hashes = [data_hashes] unless data_hashes.is_a?(Array)
- @saved = false unless @project.append_or_update_attribute(relation_key, relation_hash)
+ # consume and remove objects from memory
+ while data_hash = data_hashes.shift
+ process_project_relation_item!(relation_key, relation_definition, data_hash)
+ end
+ end
- save_id_mappings(relation_key, relation_hash_batch, relation_hash)
+ def process_project_relation_item!(relation_key, relation_definition, data_hash)
+ relation_object = build_relation(relation_key, relation_definition, data_hash)
+ return unless relation_object
+ return if group_model?(relation_object)
- @project.reset
+ relation_object.project = @project
+ relation_object.save!
+
+ save_id_mapping(relation_key, data_hash, relation_object)
end
# Older, serialized CI pipeline exports may only have a
# merge_request_id and not the full hash of the merge request. To
# import these pipelines, we need to preserve the mapping between
# the old and new the merge request ID.
- def save_id_mappings(relation_key, relation_hash_batch, relation_hash)
+ def save_id_mapping(relation_key, data_hash, relation_object)
return unless relation_key == 'merge_requests'
- relation_hash = Array(relation_hash)
-
- Array(relation_hash_batch).each_with_index do |raw_data, index|
- merge_requests_mapping[raw_data['id']] = relation_hash[index]['id']
- end
- end
-
- # Remove project models that became group models as we found them at group level.
- # This no longer required saving them at the root project level.
- # For example, in the case of an existing group label that matched the title.
- def remove_group_models(relation_hash)
- relation_hash.reject! do |value|
- GROUP_MODELS.include?(value.class) && value.group_id
- end
- end
-
- def remove_feature_dependent_sub_relations!(_relation_item)
- # no-op
+ merge_requests_mapping[data_hash['id']] = relation_object.id
end
def project_relations
- @project_relations ||= reader.attributes_finder.find_relations_tree(:project)
+ @project_relations ||=
+ reader
+ .attributes_finder
+ .find_relations_tree(:project)
+ .deep_stringify_keys
end
def update_project_params!
- Gitlab::Timeless.timeless(@project) do
- project_params = @tree_hash.reject do |key, value|
- project_relations.include?(key.to_sym)
- end
+ project_params = @tree_hash.reject do |key, value|
+ project_relations.include?(key)
+ end
- project_params = project_params.merge(present_project_override_params)
+ project_params = project_params.merge(
+ present_project_override_params)
- # Cleaning all imported and overridden params
- project_params = Gitlab::ImportExport::AttributeCleaner.clean(
- relation_hash: project_params,
- relation_class: Project,
- excluded_keys: excluded_keys_for_relation(:project))
+ # Cleaning all imported and overridden params
+ project_params = Gitlab::ImportExport::AttributeCleaner.clean(
+ relation_hash: project_params,
+ relation_class: Project,
+ excluded_keys: excluded_keys_for_relation(:project))
- @project.assign_attributes(project_params)
- @project.drop_visibility_level!
+ @project.assign_attributes(project_params)
+ @project.drop_visibility_level!
+
+ Gitlab::Timeless.timeless(@project) do
@project.save!
end
end
@@ -160,75 +154,61 @@ module Gitlab
@project_override_params ||= @project.import_data&.data&.fetch('override_params', nil) || {}
end
- # Given a relation hash containing one or more models and its relationships,
- # loops through each model and each object from a model type and
- # and assigns its correspondent attributes hash from +tree_hash+
- # Example:
- # +relation_key+ issues, loops through the list of *issues* and for each individual
- # issue, finds any subrelations such as notes, creates them and assign them back to the hash
- #
- # Recursively calls this method if the sub-relation is a hash containing more sub-relations
- def create_sub_relations(relation_key, relation_definition, tree_hash, save: true)
- return if tree_hash[relation_key].blank?
-
- tree_array = [tree_hash[relation_key]].flatten
-
- # Avoid keeping a possible heavy object in memory once we are done with it
- while relation_item = tree_array.shift
- remove_feature_dependent_sub_relations!(relation_item)
-
- # The transaction at this level is less speedy than one single transaction
- # But we can't have it in the upper level or GC won't get rid of the AR objects
- # after we save the batch.
- Project.transaction do
- process_sub_relation(relation_key, relation_definition, relation_item)
-
- # For every subrelation that hangs from Project, save the associated records altogether
- # This effectively batches all records per subrelation item, only keeping those in memory
- # We have to keep in mind that more batch granularity << Memory, but >> Slowness
- if save
- save_relation_hash(relation_key, [relation_item])
- tree_hash[relation_key].delete(relation_item)
- end
- end
- end
-
- tree_hash.delete(relation_key) if save
+ def build_relations(relation_key, relation_definition, data_hashes)
+ data_hashes.map do |data_hash|
+ build_relation(relation_key, relation_definition, data_hash)
+ end.compact
end
- def process_sub_relation(relation_key, relation_definition, relation_item)
- relation_definition.each do |sub_relation_key, sub_relation_definition|
- # We just use author to get the user ID, do not attempt to create an instance.
- next if sub_relation_key == :author
+ def build_relation(relation_key, relation_definition, data_hash)
+ # TODO: This is hack to not create relation for the author
+ # Rather make `RelationFactory#set_note_author` to take care of that
+ return data_hash if relation_key == 'author'
- sub_relation_key_s = sub_relation_key.to_s
+ # create relation objects recursively for all sub-objects
+ relation_definition.each do |sub_relation_key, sub_relation_definition|
+ transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition)
+ end
- # create dependent relations if present
- if sub_relation_definition.present?
- create_sub_relations(sub_relation_key_s, sub_relation_definition, relation_item, save: false)
+ Gitlab::ImportExport::RelationFactory.create(
+ relation_sym: relation_key.to_sym,
+ relation_hash: data_hash,
+ members_mapper: members_mapper,
+ merge_requests_mapping: merge_requests_mapping,
+ user: @user,
+ project: @project,
+ excluded_keys: excluded_keys_for_relation(relation_key))
+ end
+
+ def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition)
+ sub_data_hash = data_hash[sub_relation_key]
+ return unless sub_data_hash
+
+ # if object is a hash we can create simple object
+ # as it means that this is 1-to-1 vs 1-to-many
+ sub_data_hash =
+ if sub_data_hash.is_a?(Array)
+ build_relations(
+ sub_relation_key,
+ sub_relation_definition,
+ sub_data_hash).presence
+ else
+ build_relation(
+ sub_relation_key,
+ sub_relation_definition,
+ sub_data_hash)
end
- # transform relation hash to actual object
- sub_relation_hash = relation_item[sub_relation_key_s]
- if sub_relation_hash.present?
- relation_item[sub_relation_key_s] = create_relation(sub_relation_key, sub_relation_hash)
- end
+ # persist object(s) or delete from relation
+ if sub_data_hash
+ data_hash[sub_relation_key] = sub_data_hash
+ else
+ data_hash.delete(sub_relation_key)
end
end
- def create_relation(relation_key, relation_hash_list)
- relation_array = [relation_hash_list].flatten.map do |relation_hash|
- Gitlab::ImportExport::RelationFactory.create(
- relation_sym: relation_key.to_sym,
- relation_hash: relation_hash,
- members_mapper: members_mapper,
- merge_requests_mapping: merge_requests_mapping,
- user: @user,
- project: @project,
- excluded_keys: excluded_keys_for_relation(relation_key))
- end.compact
-
- relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
+ def group_model?(relation_object)
+ GROUP_MODELS.include?(relation_object.class) && relation_object.group_id
end
def reader
@@ -241,5 +221,3 @@ module Gitlab
end
end
end
-
-Gitlab::ImportExport::ProjectTreeRestorer.prepend_if_ee('::EE::Gitlab::ImportExport::ProjectTreeRestorer')
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 63c71105efe..386a4cfdfc6 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -3,25 +3,20 @@
module Gitlab
module ImportExport
class ProjectTreeSaver
- include Gitlab::ImportExport::CommandLineUtil
-
attr_reader :full_path
def initialize(project:, current_user:, shared:, params: {})
- @params = params
- @project = project
+ @params = params
+ @project = project
@current_user = current_user
- @shared = shared
- @full_path = File.join(@shared.export_path, ImportExport.project_filename)
+ @shared = shared
+ @full_path = File.join(@shared.export_path, ImportExport.project_filename)
end
def save
- mkdir_p(@shared.export_path)
-
- project_tree = serialize_project_tree
+ project_tree = tree_saver.serialize(@project, reader.project_tree)
fix_project_tree(project_tree)
- project_tree_json = JSON.generate(project_tree)
- File.write(full_path, project_tree_json)
+ tree_saver.save(project_tree, @shared.export_path, ImportExport.project_filename)
true
rescue => e
@@ -43,16 +38,6 @@ module Gitlab
RelationRenameService.add_new_associations(project_tree)
end
- def serialize_project_tree
- if Feature.enabled?(:export_fast_serialize, default_enabled: true)
- Gitlab::ImportExport::FastHashSerializer
- .new(@project, reader.project_tree)
- .execute
- else
- @project.as_json(reader.project_tree)
- end
- end
-
def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
@@ -74,6 +59,10 @@ module Gitlab
GroupMembersFinder.new(@project.group).execute.where.not(user_id: non_null_user_ids)
end
+
+ def tree_saver
+ @tree_saver ||= RelationTreeSaver.new
+ end
end
end
end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index 9e81c6a3d07..1390770acef 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -5,24 +5,31 @@ module Gitlab
class Reader
attr_reader :tree, :attributes_finder
- def initialize(shared:)
- @shared = shared
-
- @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(
- config: ImportExport::Config.new.to_h)
+ def initialize(shared:, config: ImportExport::Config.new.to_h)
+ @shared = shared
+ @config = config
+ @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(config: @config)
end
# Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
# for outputting a project in JSON format, including its relations and sub relations.
def project_tree
- attributes_finder.find_root(:project)
- rescue => e
- @shared.error(e)
- false
+ tree_by_key(:project)
+ end
+
+ def group_tree
+ tree_by_key(:group)
end
def group_members_tree
- attributes_finder.find_root(:group_members)
+ tree_by_key(:group_members)
+ end
+
+ def tree_by_key(key)
+ attributes_finder.find_root(key)
+ rescue => e
+ @shared.error(e)
+ false
end
end
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index ae8025c52ef..ae6b3c161ce 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -38,10 +38,13 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
- EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature merge_request].freeze
+ EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature merge_request ProjectCiCdSetting].freeze
TOKEN_RESET_MODELS = %i[Project Namespace Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
+ # This represents all relations that have unique key on `project_id`
+ UNIQUE_RELATIONS = %i[project_feature ProjectCiCdSetting].freeze
+
def self.create(*args)
new(*args).create
end
@@ -274,7 +277,7 @@ module Gitlab
end
def setup_pipeline
- @relation_hash.fetch('stages').each do |stage|
+ @relation_hash.fetch('stages', []).each do |stage|
stage.statuses.each do |status|
status.pipeline = imported_object
end
@@ -324,8 +327,7 @@ module Gitlab
end
def find_or_create_object!
- return relation_class.find_or_create_by(project_id: @project.id) if @relation_name == :project_feature
- return find_or_create_merge_request! if @relation_name == :merge_request
+ return relation_class.find_or_create_by(project_id: @project.id) if UNIQUE_RELATIONS.include?(@relation_name)
# Can't use IDs as validation exists calling `group` or `project` attributes
finder_hash = parsed_relation_hash.tap do |hash|
@@ -336,11 +338,6 @@ module Gitlab
GroupProjectObjectBuilder.build(relation_class, finder_hash)
end
-
- def find_or_create_merge_request!
- @project.merge_requests.find_by(iid: parsed_relation_hash['iid']) ||
- relation_class.new(parsed_relation_hash)
- end
end
end
end
diff --git a/lib/gitlab/import_export/relation_rename_service.rb b/lib/gitlab/import_export/relation_rename_service.rb
index 179bde5e21e..03aaa6aefc3 100644
--- a/lib/gitlab/import_export/relation_rename_service.rb
+++ b/lib/gitlab/import_export/relation_rename_service.rb
@@ -8,7 +8,7 @@
# The behavior of these renamed relationships should be transient and it should
# only last one release until you completely remove the renaming from the list.
#
-# When importing, this class will check the project hash and:
+# When importing, this class will check the hash and:
# - if only the old relationship name is found, it will rename it with the new one
# - if only the new relationship name is found, it will do nothing
# - if it finds both, it will use the new relationship data
diff --git a/lib/gitlab/import_export/relation_tree_saver.rb b/lib/gitlab/import_export/relation_tree_saver.rb
new file mode 100644
index 00000000000..a0452071ccf
--- /dev/null
+++ b/lib/gitlab/import_export/relation_tree_saver.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ class RelationTreeSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def serialize(exportable, relations_tree)
+ if Feature.enabled?(:export_fast_serialize, default_enabled: true)
+ Gitlab::ImportExport::FastHashSerializer
+ .new(exportable, relations_tree)
+ .execute
+ else
+ exportable.as_json(relations_tree)
+ end
+ end
+
+ def save(tree, dir_path, filename)
+ mkdir_p(dir_path)
+
+ tree_json = JSON.generate(tree)
+
+ File.write(File.join(dir_path, filename), tree_json)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb
index bea7a7cce65..ae82c380755 100644
--- a/lib/gitlab/import_export/saver.rb
+++ b/lib/gitlab/import_export/saver.rb
@@ -9,16 +9,16 @@ module Gitlab
new(*args).save
end
- def initialize(project:, shared:)
- @project = project
- @shared = shared
+ def initialize(exportable:, shared:)
+ @exportable = exportable
+ @shared = shared
end
def save
if compress_and_save
remove_export_path
- Rails.logger.info("Saved project export #{archive_file}") # rubocop:disable Gitlab/RailsLogger
+ Rails.logger.info("Saved #{@exportable.class} export #{archive_file}") # rubocop:disable Gitlab/RailsLogger
save_upload
else
@@ -48,11 +48,11 @@ module Gitlab
end
def archive_file
- @archive_file ||= File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(project: @project))
+ @archive_file ||= File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(exportable: @exportable))
end
def save_upload
- upload = ImportExportUpload.find_or_initialize_by(project: @project)
+ upload = initialize_upload
File.open(archive_file) { |file| upload.export_file = file }
@@ -62,6 +62,12 @@ module Gitlab
def error_message
"Unable to save #{archive_file} into #{@shared.export_path}."
end
+
+ def initialize_upload
+ exportable_kind = @exportable.class.name.downcase
+
+ ImportExportUpload.find_or_initialize_by(Hash[exportable_kind, @exportable])
+ end
end
end
end
diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb
index 02d46a1f498..2539a6828c3 100644
--- a/lib/gitlab/import_export/shared.rb
+++ b/lib/gitlab/import_export/shared.rb
@@ -23,21 +23,21 @@
module Gitlab
module ImportExport
class Shared
- attr_reader :errors, :project
+ attr_reader :errors, :exportable, :logger
LOCKS_DIRECTORY = 'locks'
- def initialize(project)
- @project = project
- @errors = []
- @logger = Gitlab::Import::Logger.build
+ def initialize(exportable)
+ @exportable = exportable
+ @errors = []
+ @logger = Gitlab::Import::Logger.build
end
def active_export_count
Dir[File.join(base_path, '*')].count { |name| File.basename(name) != LOCKS_DIRECTORY && File.directory?(name) }
end
- # The path where the project metadata and repository bundle is saved
+ # The path where the exportable metadata and repository bundle (in case of project) is saved
def export_path
@export_path ||= Gitlab::ImportExport.export_path(relative_path: relative_path)
end
@@ -84,11 +84,18 @@ module Gitlab
end
def relative_archive_path
- @relative_archive_path ||= File.join(@project.disk_path, SecureRandom.hex)
+ @relative_archive_path ||= File.join(relative_base_path, SecureRandom.hex)
end
def relative_base_path
- @project.disk_path
+ case exportable_type
+ when 'Project'
+ @exportable.disk_path
+ when 'Group'
+ @exportable.full_path
+ else
+ raise Gitlab::ImportExport::Error.new("Unsupported Exportable Type #{@exportable&.class}")
+ end
end
def log_error(details)
@@ -100,17 +107,24 @@ module Gitlab
end
def log_base_data
- {
- importer: 'Import/Export',
- import_jid: @project&.import_state&.jid,
- project_id: @project&.id,
- project_path: @project&.full_path
+ log = {
+ importer: 'Import/Export',
+ exportable_id: @exportable&.id,
+ exportable_path: @exportable&.full_path
}
+
+ log[:import_jid] = @exportable&.import_state&.jid if exportable_type == 'Project'
+
+ log
end
def filtered_error_message(message)
Projects::ImportErrorFilter.filter_message(message)
end
+
+ def exportable_type
+ @exportable.class.name
+ end
end
end
end
diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb
index e6a5facb2a5..edaa9c645b4 100644
--- a/lib/gitlab/instrumentation_helper.rb
+++ b/lib/gitlab/instrumentation_helper.rb
@@ -21,5 +21,49 @@ module Gitlab
payload[:rugged_duration_ms] = Gitlab::RuggedInstrumentation.query_time_ms
end
end
+
+ # Returns the queuing duration for a Sidekiq job in seconds, as a float, if the
+ # `enqueued_at` field or `created_at` field is available.
+ #
+ # * If the job doesn't contain sufficient information, returns nil
+ # * If the job has a start time in the future, returns 0
+ # * If the job contains an invalid start time value, returns nil
+ # @param [Hash] job a Sidekiq job, represented as a hash
+ def self.queue_duration_for_job(job)
+ # Old gitlab-shell messages don't provide enqueued_at/created_at attributes
+ enqueued_at = job['enqueued_at'] || job['created_at']
+ return unless enqueued_at
+
+ enqueued_at_time = convert_to_time(enqueued_at)
+ return unless enqueued_at_time
+
+ # Its possible that if theres clock-skew between two nodes
+ # this value may be less than zero. In that event, we record the value
+ # as zero.
+ [elapsed_by_absolute_time(enqueued_at_time), 0].max
+ end
+
+ # Calculates the time in seconds, as a float, from
+ # the provided start time until now
+ #
+ # @param [Time] start
+ def self.elapsed_by_absolute_time(start)
+ (Time.now - start).to_f.round(6)
+ end
+ private_class_method :elapsed_by_absolute_time
+
+ # Convert a representation of a time into a `Time` value
+ #
+ # @param time_value String, Float time representation, or nil
+ def self.convert_to_time(time_value)
+ return time_value if time_value.is_a?(Time)
+ return Time.iso8601(time_value) if time_value.is_a?(String)
+ return Time.at(time_value) if time_value.is_a?(Numeric) && time_value > 0
+ rescue ArgumentError
+ # Swallow invalid dates. Better to loose some observability
+ # than bring all background processing down because of a date
+ # formatting bug in a client
+ end
+ private_class_method :convert_to_time
end
end
diff --git a/lib/gitlab/kubernetes/config_maps/aws_node_auth.rb b/lib/gitlab/kubernetes/config_maps/aws_node_auth.rb
new file mode 100644
index 00000000000..ef51cee09ca
--- /dev/null
+++ b/lib/gitlab/kubernetes/config_maps/aws_node_auth.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ module ConfigMaps
+ class AwsNodeAuth
+ attr_reader :node_role
+
+ def initialize(node_role)
+ @node_role = node_role
+ end
+
+ def generate
+ Kubeclient::Resource.new(
+ metadata: metadata,
+ data: data
+ )
+ end
+
+ private
+
+ def metadata
+ {
+ 'name' => 'aws-auth',
+ 'namespace' => 'kube-system'
+ }
+ end
+
+ def data
+ { 'mapRoles' => instance_role_config(node_role) }
+ end
+
+ def instance_role_config(role)
+ [{
+ 'rolearn' => role,
+ 'username' => 'system:node:{{EC2PrivateDNSName}}',
+ 'groups' => [
+ 'system:bootstrappers',
+ 'system:nodes'
+ ]
+ }].to_yaml
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
index 16ed0cb0f8e..b5181670b93 100644
--- a/lib/gitlab/kubernetes/helm.rb
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -3,8 +3,8 @@
module Gitlab
module Kubernetes
module Helm
- HELM_VERSION = '2.14.3'
- KUBECTL_VERSION = '1.11.10'
+ HELM_VERSION = '2.16.1'
+ KUBECTL_VERSION = '1.13.12'
NAMESPACE = 'gitlab-managed-apps'
SERVICE_ACCOUNT = 'tiller'
CLUSTER_ROLE_BINDING = 'tiller-admin'
diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb
index f572bc43533..ccb053f507d 100644
--- a/lib/gitlab/kubernetes/helm/install_command.rb
+++ b/lib/gitlab/kubernetes/helm/install_command.rb
@@ -40,7 +40,7 @@ module Gitlab
private
def repository_update_command
- 'helm repo update' if repository
+ 'helm repo update'
end
# Uses `helm upgrade --install` which means we can use this for both
diff --git a/lib/gitlab/metrics/dashboard/errors.rb b/lib/gitlab/metrics/dashboard/errors.rb
index d41bd2c43c7..264ea0488e7 100644
--- a/lib/gitlab/metrics/dashboard/errors.rb
+++ b/lib/gitlab/metrics/dashboard/errors.rb
@@ -9,6 +9,7 @@ module Gitlab
module Errors
DashboardProcessingError = Class.new(StandardError)
PanelNotFoundError = Class.new(StandardError)
+ MissingIntegrationError = Class.new(StandardError)
LayoutError = Class.new(DashboardProcessingError)
MissingQueryError = Class.new(DashboardProcessingError)
@@ -22,6 +23,10 @@ module Gitlab
error("#{dashboard_path} could not be found.", :not_found)
when PanelNotFoundError
error(error.message, :not_found)
+ when ::Grafana::Client::Error
+ error(error.message, :service_unavailable)
+ when MissingIntegrationError
+ error('Proxy support for this API is not available currently', :bad_request)
else
raise error
end
diff --git a/lib/gitlab/metrics/dashboard/finder.rb b/lib/gitlab/metrics/dashboard/finder.rb
index 297f109ff81..268112f33a9 100644
--- a/lib/gitlab/metrics/dashboard/finder.rb
+++ b/lib/gitlab/metrics/dashboard/finder.rb
@@ -12,6 +12,7 @@ module Gitlab
# @param project [Project]
# @param user [User]
# @param environment [Environment]
+ # @param options [Hash<Symbol,Any>]
# @param options - embedded [Boolean] Determines whether the
# dashboard is to be rendered as part of an
# issue or location other than the primary
@@ -31,6 +32,8 @@ module Gitlab
# @param options - cluster [Cluster]
# @param options - cluster_type [Symbol] The level of
# cluster, one of [:admin, :project, :group]
+ # @param options - grafana_url [String] URL pointing
+ # to a grafana dashboard panel
# @return [Hash]
def find(project, user, options = {})
service_for(options)
diff --git a/lib/gitlab/metrics/dashboard/processor.rb b/lib/gitlab/metrics/dashboard/processor.rb
index bfdee76a818..9566e5afb9a 100644
--- a/lib/gitlab/metrics/dashboard/processor.rb
+++ b/lib/gitlab/metrics/dashboard/processor.rb
@@ -17,7 +17,10 @@ module Gitlab
# Returns a new dashboard hash with the results of
# running transforms on the dashboard.
+ # @return [Hash, nil]
def process
+ return unless @dashboard
+
@dashboard.deep_symbolize_keys.tap do |dashboard|
@sequence.each do |stage|
stage.new(@project, dashboard, @params).transform!
diff --git a/lib/gitlab/metrics/dashboard/service_selector.rb b/lib/gitlab/metrics/dashboard/service_selector.rb
index 10b686fbb81..aee7f6685ad 100644
--- a/lib/gitlab/metrics/dashboard/service_selector.rb
+++ b/lib/gitlab/metrics/dashboard/service_selector.rb
@@ -18,6 +18,7 @@ module Gitlab
# @return [Gitlab::Metrics::Dashboard::Services::BaseService]
def call(params)
return SERVICES::CustomMetricEmbedService if custom_metric_embed?(params)
+ return SERVICES::GrafanaMetricEmbedService if grafana_metric_embed?(params)
return SERVICES::DynamicEmbedService if dynamic_embed?(params)
return SERVICES::DefaultEmbedService if params[:embedded]
return SERVICES::SystemDashboardService if system_dashboard?(params[:dashboard_path])
@@ -40,6 +41,10 @@ module Gitlab
SERVICES::CustomMetricEmbedService.valid_params?(params)
end
+ def grafana_metric_embed?(params)
+ SERVICES::GrafanaMetricEmbedService.valid_params?(params)
+ end
+
def dynamic_embed?(params)
SERVICES::DynamicEmbedService.valid_params?(params)
end
diff --git a/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
index 188912bedb4..62479ed6de4 100644
--- a/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
+++ b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
@@ -9,7 +9,7 @@ module Gitlab
# find a corresponding database record. If found,
# includes the record's id in the dashboard config.
def transform!
- common_metrics = ::PrometheusMetric.common
+ common_metrics = ::PrometheusMetricsFinder.new(common: true).execute
for_metrics do |metric|
metric_record = common_metrics.find { |m| m.identifier == metric[:id] }
diff --git a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
new file mode 100644
index 00000000000..ce75c54d014
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class GrafanaFormatter < BaseStage
+ include Gitlab::Utils::StrongMemoize
+
+ CHART_TYPE = 'area-chart'
+ PROXY_PATH = 'api/v1/query_range'
+
+ # Reformats the specified panel in the Gitlab
+ # dashboard-yml format
+ def transform!
+ InputFormatValidator.new(
+ grafana_dashboard,
+ datasource,
+ panel,
+ query_params
+ ).validate!
+
+ new_dashboard = formatted_dashboard
+
+ dashboard.clear
+ dashboard.merge!(new_dashboard)
+ end
+
+ private
+
+ def formatted_dashboard
+ { panel_groups: [{ panels: [formatted_panel] }] }
+ end
+
+ def formatted_panel
+ {
+ title: panel[:title],
+ type: CHART_TYPE,
+ y_label: '', # Grafana panels do not include a Y-Axis label
+ metrics: panel[:targets].map.with_index do |target, idx|
+ formatted_metric(target, idx)
+ end
+ }
+ end
+
+ def formatted_metric(metric, idx)
+ {
+ id: "#{metric[:legendFormat]}_#{idx}",
+ query_range: format_query(metric),
+ label: replace_variables(metric[:legendFormat]),
+ prometheus_endpoint_path: prometheus_endpoint_for_metric(metric)
+ }.compact
+ end
+
+ # Panel specified by the url from the Grafana dashboard
+ def panel
+ strong_memoize(:panel) do
+ grafana_dashboard[:dashboard][:panels].find do |panel|
+ panel[:id].to_s == query_params[:panelId]
+ end
+ end
+ end
+
+ # Grafana url query parameters. Includes information
+ # on which panel to select and time range.
+ def query_params
+ strong_memoize(:query_params) do
+ Gitlab::Metrics::Dashboard::Url.parse_query(grafana_url)
+ end
+ end
+
+ # Endpoint which will return prometheus metric data
+ # for the metric
+ def prometheus_endpoint_for_metric(metric)
+ Gitlab::Routing.url_helpers.project_grafana_api_path(
+ project,
+ datasource_id: datasource[:id],
+ proxy_path: PROXY_PATH,
+ query: format_query(metric)
+ )
+ end
+
+ # Reformats query for compatibility with prometheus api.
+ def format_query(metric)
+ expression = remove_new_lines(metric[:expr])
+ expression = replace_variables(expression)
+ expression = replace_global_variables(expression, metric)
+
+ expression
+ end
+
+ # Accomodates instance-defined Grafana variables.
+ # These are variables defined by users, and values
+ # must be provided in the query parameters.
+ def replace_variables(expression)
+ return expression unless grafana_dashboard[:dashboard][:templating]
+
+ grafana_dashboard[:dashboard][:templating][:list]
+ .sort_by { |variable| variable[:name].length }
+ .each do |variable|
+ variable_value = query_params[:"var-#{variable[:name]}"]
+
+ expression = expression.gsub("$#{variable[:name]}", variable_value)
+ expression = expression.gsub("[[#{variable[:name]}]]", variable_value)
+ expression = expression.gsub("{{#{variable[:name]}}}", variable_value)
+ end
+
+ expression
+ end
+
+ # Replaces Grafana global built-in variables with values.
+ # Only $__interval and $__from and $__to are supported.
+ #
+ # See https://grafana.com/docs/reference/templating/#global-built-in-variables
+ def replace_global_variables(expression, metric)
+ expression = expression.gsub('$__interval', metric[:interval]) if metric[:interval]
+ expression = expression.gsub('$__from', query_params[:from])
+ expression = expression.gsub('$__to', query_params[:to])
+
+ expression
+ end
+
+ # Removes new lines from expression.
+ def remove_new_lines(expression)
+ expression.gsub(/\R+/, '')
+ end
+
+ # Grafana datasource object corresponding to the
+ # specified dashboard
+ def datasource
+ params[:datasource]
+ end
+
+ # The specified Grafana dashboard
+ def grafana_dashboard
+ params[:grafana_dashboard]
+ end
+
+ # The URL specifying which Grafana panel to embed
+ def grafana_url
+ params[:grafana_url]
+ end
+ end
+
+ class InputFormatValidator
+ include ::Gitlab::Metrics::Dashboard::Errors
+
+ attr_reader :grafana_dashboard, :datasource, :panel, :query_params
+
+ UNSUPPORTED_GRAFANA_GLOBAL_VARS = %w(
+ $__interval_ms
+ $__timeFilter
+ $__name
+ $timeFilter
+ $interval
+ ).freeze
+
+ def initialize(grafana_dashboard, datasource, panel, query_params)
+ @grafana_dashboard = grafana_dashboard
+ @datasource = datasource
+ @panel = panel
+ @query_params = query_params
+ end
+
+ def validate!
+ validate_query_params!
+ validate_datasource!
+ validate_panel_type!
+ validate_variable_definitions!
+ validate_global_variables!
+ end
+
+ private
+
+ def validate_datasource!
+ return if datasource[:access] == 'proxy' && datasource[:type] == 'prometheus'
+
+ raise_error 'Only Prometheus datasources with proxy access in Grafana are supported.'
+ end
+
+ def validate_query_params!
+ return if [:panelId, :from, :to].all? { |param| query_params.include?(param) }
+
+ raise_error 'Grafana query parameters must include panelId, from, and to.'
+ end
+
+ def validate_panel_type!
+ return if panel[:type] == 'graph' && panel[:lines]
+
+ raise_error 'Panel type must be a line graph.'
+ end
+
+ def validate_variable_definitions!
+ return unless grafana_dashboard[:dashboard][:templating]
+
+ return if grafana_dashboard[:dashboard][:templating][:list].all? do |variable|
+ query_params[:"var-#{variable[:name]}"].present?
+ end
+
+ raise_error 'All Grafana variables must be defined in the query parameters.'
+ end
+
+ def validate_global_variables!
+ return unless panel_contains_unsupported_vars?
+
+ raise_error 'Prometheus must not include'
+ end
+
+ def panel_contains_unsupported_vars?
+ panel[:targets].any? do |target|
+ UNSUPPORTED_GRAFANA_GLOBAL_VARS.any? do |variable|
+ target[:expr].include?(variable)
+ end
+ end
+ end
+
+ def raise_error(message)
+ raise DashboardProcessingError.new(message)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb b/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb
index 643be309992..c0f67d445f8 100644
--- a/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb
+++ b/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb
@@ -9,7 +9,7 @@ module Gitlab
# config. If there are no project-specific metrics,
# this will have no effect.
def transform!
- project.prometheus_metrics.each do |project_metric|
+ PrometheusMetricsFinder.new(project: project).execute.each do |project_metric|
group = find_or_create_panel_group(dashboard[:panel_groups], project_metric)
panel = find_or_create_panel(group[:panels], project_metric)
find_or_create_metric(panel[:metrics], project_metric)
diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb
index 94f8b2e02b1..712f769bbeb 100644
--- a/lib/gitlab/metrics/dashboard/url.rb
+++ b/lib/gitlab/metrics/dashboard/url.rb
@@ -14,17 +14,31 @@ module Gitlab
def regex
%r{
(?<url>
- #{Regexp.escape(Gitlab.config.gitlab.url)}
- \/#{Project.reference_pattern}
+ #{gitlab_pattern}
+ #{project_pattern}
(?:\/\-)?
\/environments
\/(?<environment>\d+)
\/metrics
- (?<query>
- \?[a-zA-Z0-9%.()+_=-]+
- (&[a-zA-Z0-9%.()+_=-]+)*
- )?
- (?<anchor>\#[a-z0-9_-]+)?
+ #{query_pattern}
+ #{anchor_pattern}
+ )
+ }x
+ end
+
+ # Matches dashboard urls for a Grafana embed.
+ #
+ # EX - https://<host>/<namespace>/<project>/grafana/metrics_dashboard
+ def grafana_regex
+ %r{
+ (?<url>
+ #{gitlab_pattern}
+ #{project_pattern}
+ (?:\/\-)?
+ \/grafana
+ \/metrics_dashboard
+ #{query_pattern}
+ #{anchor_pattern}
)
}x
end
@@ -45,6 +59,24 @@ module Gitlab
def build_dashboard_url(*args)
Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_environment_url(*args)
end
+
+ private
+
+ def gitlab_pattern
+ Regexp.escape(Gitlab.config.gitlab.url)
+ end
+
+ def project_pattern
+ "\/#{Project.reference_pattern}"
+ end
+
+ def query_pattern
+ '(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?'
+ end
+
+ def anchor_pattern
+ '(?<anchor>\#[a-z0-9_-]+)?'
+ end
end
end
end
diff --git a/lib/gitlab/metrics/exporter/web_exporter.rb b/lib/gitlab/metrics/exporter/web_exporter.rb
index 3940f6fa155..b6a27d8556a 100644
--- a/lib/gitlab/metrics/exporter/web_exporter.rb
+++ b/lib/gitlab/metrics/exporter/web_exporter.rb
@@ -20,6 +20,10 @@ module Gitlab
def initialize
super
+ # DEPRECATED:
+ # these `readiness_checks` are deprecated
+ # as presenting no value in a way how we run
+ # application: https://gitlab.com/gitlab-org/gitlab/issues/35343
self.readiness_checks = [
WebExporter::ExporterCheck.new(self),
Gitlab::HealthChecks::PumaCheck,
@@ -35,6 +39,10 @@ module Gitlab
File.join(Rails.root, 'log', 'web_exporter.log')
end
+ def mark_as_not_running!
+ @running = false
+ end
+
private
def start_working
@@ -43,24 +51,9 @@ module Gitlab
end
def stop_working
- @running = false
- wait_in_blackout_period if server && thread.alive?
+ mark_as_not_running!
super
end
-
- def wait_in_blackout_period
- return unless blackout_seconds > 0
-
- @server.logger.info(
- message: 'starting blackout...',
- duration_s: blackout_seconds)
-
- sleep(blackout_seconds)
- end
-
- def blackout_seconds
- settings['blackout_seconds'].to_i
- end
end
end
end
diff --git a/lib/gitlab/metrics/requests_rack_middleware.rb b/lib/gitlab/metrics/requests_rack_middleware.rb
index 085e28123a7..b57f9a19f8e 100644
--- a/lib/gitlab/metrics/requests_rack_middleware.rb
+++ b/lib/gitlab/metrics/requests_rack_middleware.rb
@@ -35,7 +35,7 @@ module Gitlab
def self.initialize_http_request_duration_seconds
HTTP_METHODS.each do |method, statuses|
statuses.each do |status|
- http_request_duration_seconds.get({ method: method, status: status.to_i })
+ http_request_duration_seconds.get({ method: method, status: status.to_s })
end
end
end
@@ -49,7 +49,7 @@ module Gitlab
status, headers, body = @app.call(env)
elapsed = Time.now.to_f - started
- RequestsRackMiddleware.http_request_duration_seconds.observe({ method: method, status: status }, elapsed)
+ RequestsRackMiddleware.http_request_duration_seconds.observe({ method: method, status: status.to_s }, elapsed)
[status, headers, body]
rescue
diff --git a/lib/gitlab/pagination/base.rb b/lib/gitlab/pagination/base.rb
new file mode 100644
index 00000000000..90fa1f8d1ec
--- /dev/null
+++ b/lib/gitlab/pagination/base.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pagination
+ class Base
+ private
+
+ def per_page
+ @per_page ||= params[:per_page]
+ end
+
+ def base_request_uri
+ @base_request_uri ||= URI.parse(request.url).tap do |uri|
+ uri.host = Gitlab.config.gitlab.host
+ uri.port = Gitlab.config.gitlab.port
+ end
+ end
+
+ def build_page_url(query_params:)
+ base_request_uri.tap do |uri|
+ uri.query = query_params
+ end.to_s
+ end
+
+ def page_href(next_page_params = {})
+ query_params = params.merge(**next_page_params, per_page: per_page).to_query
+
+ build_page_url(query_params: query_params)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pagination/offset_pagination.rb b/lib/gitlab/pagination/offset_pagination.rb
new file mode 100644
index 00000000000..bf31f252a6b
--- /dev/null
+++ b/lib/gitlab/pagination/offset_pagination.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pagination
+ class OffsetPagination < Base
+ attr_reader :request_context
+ delegate :params, :header, :request, to: :request_context
+
+ def initialize(request_context)
+ @request_context = request_context
+ end
+
+ def paginate(relation)
+ paginate_with_limit_optimization(add_default_order(relation)).tap do |data|
+ add_pagination_headers(data)
+ end
+ end
+
+ private
+
+ def paginate_with_limit_optimization(relation)
+ pagination_data = relation.page(params[:page]).per(params[:per_page])
+ return pagination_data unless pagination_data.is_a?(ActiveRecord::Relation)
+ return pagination_data unless Feature.enabled?(:api_kaminari_count_with_limit)
+
+ limited_total_count = pagination_data.total_count_with_limit
+ if limited_total_count > Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT
+ # The call to `total_count_with_limit` memoizes `@arel` because of a call to `references_eager_loaded_tables?`
+ # We need to call `reset` because `without_count` relies on `@arel` being unmemoized
+ pagination_data.reset.without_count
+ else
+ pagination_data
+ end
+ end
+
+ def add_default_order(relation)
+ if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty?
+ relation = relation.order(:id) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ relation
+ end
+
+ def add_pagination_headers(paginated_data)
+ header 'X-Per-Page', paginated_data.limit_value.to_s
+ header 'X-Page', paginated_data.current_page.to_s
+ header 'X-Next-Page', paginated_data.next_page.to_s
+ header 'X-Prev-Page', paginated_data.prev_page.to_s
+ header 'Link', pagination_links(paginated_data)
+
+ return if data_without_counts?(paginated_data)
+
+ header 'X-Total', paginated_data.total_count.to_s
+ header 'X-Total-Pages', total_pages(paginated_data).to_s
+ end
+
+ def pagination_links(paginated_data)
+ [].tap do |links|
+ links << %(<#{page_href(page: paginated_data.prev_page)}>; rel="prev") if paginated_data.prev_page
+ links << %(<#{page_href(page: paginated_data.next_page)}>; rel="next") if paginated_data.next_page
+ links << %(<#{page_href(page: 1)}>; rel="first")
+
+ links << %(<#{page_href(page: total_pages(paginated_data))}>; rel="last") unless data_without_counts?(paginated_data)
+ end.join(', ')
+ end
+
+ def total_pages(paginated_data)
+ # Ensure there is in total at least 1 page
+ [paginated_data.total_pages, 1].max
+ end
+
+ def data_without_counts?(paginated_data)
+ paginated_data.is_a?(Kaminari::PaginatableWithoutCount)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/project_authorizations.rb b/lib/gitlab/project_authorizations.rb
index a9270cd536e..4e5e2d4a6a9 100644
--- a/lib/gitlab/project_authorizations.rb
+++ b/lib/gitlab/project_authorizations.rb
@@ -57,7 +57,7 @@ module Gitlab
private
# Builds a recursive CTE that gets all the groups the current user has
- # access to, including any nested groups.
+ # access to, including any nested groups and any shared groups.
def recursive_cte
cte = Gitlab::SQL::RecursiveCTE.new(:namespaces_cte)
members = Member.arel_table
@@ -68,20 +68,27 @@ module Gitlab
.select([namespaces[:id], members[:access_level]])
.except(:order)
+ if Feature.enabled?(:share_group_with_group)
+ # Namespaces shared with any of the group
+ cte << Group.select([namespaces[:id], 'group_group_links.group_access AS access_level'])
+ .joins(join_group_group_links)
+ .joins(join_members_on_group_group_links)
+ end
+
# Sub groups of any groups the user is a member of.
cte << Group.select([
namespaces[:id],
greatest(members[:access_level], cte.table[:access_level], 'access_level')
])
.joins(join_cte(cte))
- .joins(join_members)
+ .joins(join_members_on_namespaces)
.except(:order)
cte
end
# Builds a LEFT JOIN to join optional memberships onto the CTE.
- def join_members
+ def join_members_on_namespaces
members = Member.arel_table
namespaces = Namespace.arel_table
@@ -94,6 +101,23 @@ module Gitlab
Arel::Nodes::OuterJoin.new(members, Arel::Nodes::On.new(cond))
end
+ def join_group_group_links
+ group_group_links = GroupGroupLink.arel_table
+ namespaces = Namespace.arel_table
+
+ cond = group_group_links[:shared_group_id].eq(namespaces[:id])
+ Arel::Nodes::InnerJoin.new(group_group_links, Arel::Nodes::On.new(cond))
+ end
+
+ def join_members_on_group_group_links
+ group_group_links = GroupGroupLink.arel_table
+ members = Member.arel_table
+
+ cond = group_group_links[:shared_with_group_id].eq(members[:source_id])
+ .and(members[:user_id].eq(user.id))
+ Arel::Nodes::InnerJoin.new(members, Arel::Nodes::On.new(cond))
+ end
+
# Builds an INNER JOIN to join namespaces onto the CTE.
def join_cte(cte)
namespaces = Namespace.arel_table
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index fa1d1203842..279fc4aa375 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -53,7 +53,8 @@ module Gitlab
ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'),
ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'),
ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg')
+ ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg')
].freeze
class << self
diff --git a/lib/gitlab/prometheus/internal.rb b/lib/gitlab/prometheus/internal.rb
new file mode 100644
index 00000000000..d59352119ba
--- /dev/null
+++ b/lib/gitlab/prometheus/internal.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Prometheus
+ class Internal
+ def self.uri
+ return if listen_address.blank?
+
+ if listen_address.starts_with?('0.0.0.0:')
+ # 0.0.0.0:9090
+ port = ':' + listen_address.split(':').second
+ 'http://localhost' + port
+
+ elsif listen_address.starts_with?(':')
+ # :9090
+ 'http://localhost' + listen_address
+
+ elsif listen_address.starts_with?('http')
+ # https://localhost:9090
+ listen_address
+
+ else
+ # localhost:9090
+ 'http://' + listen_address
+ end
+ end
+
+ def self.listen_address
+ Gitlab.config.prometheus.listen_address.to_s if Gitlab.config.prometheus
+ rescue Settingslogic::MissingSetting
+ Gitlab::AppLogger.error('Prometheus listen_address is not present in config/gitlab.yml')
+
+ nil
+ end
+
+ def self.prometheus_enabled?
+ Gitlab.config.prometheus.enable if Gitlab.config.prometheus
+ rescue Settingslogic::MissingSetting
+ Gitlab::AppLogger.error('prometheus.enable is not present in config/gitlab.yml')
+
+ false
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/prometheus/metric_group.rb b/lib/gitlab/prometheus/metric_group.rb
index caf0d453b6f..1b6f7282eb3 100644
--- a/lib/gitlab/prometheus/metric_group.rb
+++ b/lib/gitlab/prometheus/metric_group.rb
@@ -11,13 +11,15 @@ module Gitlab
validates :name, :priority, :metrics, presence: true
def self.common_metrics
- all_groups = ::PrometheusMetric.common.group_by(&:group_title).map do |name, metrics|
- MetricGroup.new(
- name: name,
- priority: metrics.map(&:priority).max,
- metrics: metrics.map(&:to_query_metric)
- )
- end
+ all_groups = ::PrometheusMetricsFinder.new(common: true).execute
+ .group_by(&:group_title)
+ .map do |name, metrics|
+ MetricGroup.new(
+ name: name,
+ priority: metrics.map(&:priority).max,
+ metrics: metrics.map(&:to_query_metric)
+ )
+ end
all_groups.sort_by(&:priority).reverse
end
diff --git a/lib/gitlab/prometheus/queries/knative_invocation_query.rb b/lib/gitlab/prometheus/queries/knative_invocation_query.rb
index 2691abe46d6..8873608c411 100644
--- a/lib/gitlab/prometheus/queries/knative_invocation_query.rb
+++ b/lib/gitlab/prometheus/queries/knative_invocation_query.rb
@@ -7,11 +7,14 @@ module Gitlab
include QueryAdditionalMetrics
def query(serverless_function_id)
- PrometheusMetric
- .find_by_identifier(:system_metrics_knative_function_invocation_count)
- .to_query_metric.tap do |q|
- q.queries[0][:result] = run_query(q.queries[0][:query_range], context(serverless_function_id))
- end
+ PrometheusMetricsFinder
+ .new(identifier: :system_metrics_knative_function_invocation_count, common: true)
+ .execute
+ .first
+ .to_query_metric
+ .tap do |q|
+ q.queries[0][:result] = run_query(q.queries[0][:query_range], context(serverless_function_id))
+ end
end
protected
diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb
index 340ec75c5f1..942f90e8040 100644
--- a/lib/gitlab/quick_actions/issuable_actions.rb
+++ b/lib/gitlab/quick_actions/issuable_actions.rb
@@ -234,7 +234,7 @@ module Gitlab
"#{comment} #{SHRUG}"
end
- desc _("Append the comment with %{TABLEFLIP}") % { tableflip: TABLEFLIP }
+ desc _("Append the comment with %{tableflip}") % { tableflip: TABLEFLIP }
params '<Comment>'
types Issuable
substitution :tableflip do |comment|
diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb
index 404e0c31871..838aefb59f0 100644
--- a/lib/gitlab/quick_actions/issue_actions.rb
+++ b/lib/gitlab/quick_actions/issue_actions.rb
@@ -174,18 +174,14 @@ module Gitlab
params '<Zoom URL>'
types Issue
condition do
- zoom_link_service.can_add_link?
+ @zoom_service = zoom_link_service
+ @zoom_service.can_add_link?
end
parse_params do |link|
- zoom_link_service.parse_link(link)
+ @zoom_service.parse_link(link)
end
command :zoom do |link|
- result = zoom_link_service.add_link(link)
-
- if result.success?
- @updates[:description] = result.payload[:description]
- end
-
+ result = @zoom_service.add_link(link)
@execution_message[:zoom] = result.message
end
@@ -194,15 +190,11 @@ module Gitlab
execution_message _('Zoom meeting removed')
types Issue
condition do
- zoom_link_service.can_remove_link?
+ @zoom_service = zoom_link_service
+ @zoom_service.can_remove_link?
end
command :remove_zoom do
- result = zoom_link_service.remove_link
-
- if result.success?
- @updates[:description] = result.payload[:description]
- end
-
+ result = @zoom_service.remove_link
@execution_message[:remove_zoom] = result.message
end
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index fa1615a5953..412d00c6939 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -25,6 +25,8 @@ module Gitlab
if Sidekiq.server?
# the pool will be used in a multi-threaded context
size += Sidekiq.options[:concurrency]
+ elsif defined?(::Puma)
+ size += Puma.cli_config.options[:max_threads]
end
size
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 3d1f15c72ae..e3a434dfe35 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -120,13 +120,26 @@ module Gitlab
@breakline_regex ||= /\r\n|\r|\n/
end
+ # https://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html
+ def aws_account_id_regex
+ /\A\d{12}\z/
+ end
+
+ def aws_account_id_message
+ 'must be a 12-digit number'
+ end
+
# https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
def aws_arn_regex
/\Aarn:\S+\z/
end
def aws_arn_regex_message
- "must be a valid Amazon Resource Name"
+ 'must be a valid Amazon Resource Name'
+ end
+
+ def utc_date_regex
+ @utc_date_regex ||= /\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/.freeze
end
end
end
diff --git a/lib/gitlab/search/found_blob.rb b/lib/gitlab/search/found_blob.rb
index fa09ecbdf30..360239a84e4 100644
--- a/lib/gitlab/search/found_blob.rb
+++ b/lib/gitlab/search/found_blob.rb
@@ -8,20 +8,20 @@ module Gitlab
include BlobLanguageFromGitAttributes
include Gitlab::Utils::StrongMemoize
- attr_reader :project, :content_match, :blob_filename
+ attr_reader :project, :content_match, :blob_path
- FILENAME_REGEXP = /\A(?<ref>[^:]*):(?<filename>[^\x00]*)\x00/.freeze
- CONTENT_REGEXP = /^(?<ref>[^:]*):(?<filename>[^\x00]*)\x00(?<startline>\d+)\x00/.freeze
+ PATH_REGEXP = /\A(?<ref>[^:]*):(?<path>[^\x00]*)\x00/.freeze
+ CONTENT_REGEXP = /^(?<ref>[^:]*):(?<path>[^\x00]*)\x00(?<startline>\d+)\x00/.freeze
def self.preload_blobs(blobs)
- to_fetch = blobs.select { |blob| blob.is_a?(self) && blob.blob_filename }
+ to_fetch = blobs.select { |blob| blob.is_a?(self) && blob.blob_path }
to_fetch.each { |blob| blob.fetch_blob }
end
def initialize(opts = {})
@id = opts.fetch(:id, nil)
- @binary_filename = opts.fetch(:filename, nil)
+ @binary_path = opts.fetch(:path, nil)
@binary_basename = opts.fetch(:basename, nil)
@ref = opts.fetch(:ref, nil)
@startline = opts.fetch(:startline, nil)
@@ -34,7 +34,7 @@ module Gitlab
# Allow those to just pass project_id instead.
@project_id = opts.fetch(:project_id, nil)
@content_match = opts.fetch(:content_match, nil)
- @blob_filename = opts.fetch(:blob_filename, nil)
+ @blob_path = opts.fetch(:blob_path, nil)
@repository = opts.fetch(:repository, nil)
end
@@ -50,16 +50,16 @@ module Gitlab
@startline ||= parsed_content[:startline]
end
- # binary_filename is used for running filters on all matches,
- # for grepped results (which use content_match), we get
- # filename from the beginning of the grepped result which is faster
- # then parsing whole snippet
- def binary_filename
- @binary_filename ||= content_match ? search_result_filename : parsed_content[:binary_filename]
+ # binary_path is used for running filters on all matches.
+ # For grepped results (which use content_match), we get
+ # the path from the beginning of the grepped result which is faster
+ # than parsing the whole snippet
+ def binary_path
+ @binary_path ||= content_match ? search_result_path : parsed_content[:binary_path]
end
- def filename
- @filename ||= encode_utf8(@binary_filename || parsed_content[:binary_filename])
+ def path
+ @path ||= encode_utf8(@binary_path || parsed_content[:binary_path])
end
def basename
@@ -70,10 +70,6 @@ module Gitlab
@data ||= encode_utf8(@binary_data || parsed_content[:binary_data])
end
- def path
- filename
- end
-
def project_id
@project_id || @project&.id
end
@@ -83,16 +79,16 @@ module Gitlab
end
def fetch_blob
- path = [ref, blob_filename]
- missing_blob = { binary_filename: blob_filename }
+ path = [ref, blob_path]
+ missing_blob = { binary_path: blob_path }
BatchLoader.for(path).batch(default_value: missing_blob) do |refs, loader|
Gitlab::Git::Blob.batch(repository, refs, blob_size_limit: 1024).each do |blob|
# if the blob couldn't be fetched for some reason,
- # show at least the blob filename
+ # show at least the blob path
data = {
id: blob.id,
- binary_filename: blob.path,
+ binary_path: blob.path,
binary_basename: path_without_extension(blob.path),
ref: ref,
startline: 1,
@@ -107,8 +103,8 @@ module Gitlab
private
- def search_result_filename
- content_match.match(FILENAME_REGEXP) { |matches| matches[:filename] }
+ def search_result_path
+ content_match.match(PATH_REGEXP) { |matches| matches[:path] }
end
def path_without_extension(path)
@@ -119,7 +115,7 @@ module Gitlab
strong_memoize(:parsed_content) do
if content_match
parse_search_result
- elsif blob_filename
+ elsif blob_path
fetch_blob
else
{}
@@ -129,7 +125,7 @@ module Gitlab
def parse_search_result
ref = nil
- filename = nil
+ path = nil
basename = nil
data = []
@@ -138,17 +134,17 @@ module Gitlab
content_match.each_line.each_with_index do |line, index|
prefix ||= line.match(CONTENT_REGEXP)&.tap do |matches|
ref = matches[:ref]
- filename = matches[:filename]
+ path = matches[:path]
startline = matches[:startline]
startline = startline.to_i - index
- basename = path_without_extension(filename)
+ basename = path_without_extension(path)
end
data << line.sub(prefix.to_s, '')
end
{
- binary_filename: filename,
+ binary_path: path,
binary_basename: basename,
ref: ref,
startline: startline,
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 8e2f16271eb..f96346322db 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -14,7 +14,71 @@ end
module Gitlab
class Seeder
+ extend ActionView::Helpers::NumberHelper
+
+ ESTIMATED_INSERT_PER_MINUTE = 2_000_000
+ MASS_INSERT_ENV = 'MASS_INSERT'
+
+ module ProjectSeed
+ extend ActiveSupport::Concern
+
+ included do
+ scope :not_mass_generated, -> do
+ where.not("path LIKE '#{Gitlab::Seeder::Projects::MASS_INSERT_NAME_START}%'")
+ end
+ end
+ end
+
+ module UserSeed
+ extend ActiveSupport::Concern
+
+ included do
+ scope :not_mass_generated, -> do
+ where.not("username LIKE '#{Gitlab::Seeder::Users::MASS_INSERT_USERNAME_START}%'")
+ end
+ end
+ end
+
+ def self.with_mass_insert(size, model)
+ humanized_model_name = model.is_a?(String) ? model : model.model_name.human.pluralize(size)
+
+ if !ENV[MASS_INSERT_ENV] && !ENV['CI']
+ puts "\nSkipping mass insertion for #{humanized_model_name}."
+ puts "Consider running the seed with #{MASS_INSERT_ENV}=1"
+ return
+ end
+
+ humanized_size = number_with_delimiter(size)
+ estimative = estimated_time_message(size)
+
+ puts "\nCreating #{humanized_size} #{humanized_model_name}."
+ puts estimative
+
+ yield
+
+ puts "\n#{number_with_delimiter(size)} #{humanized_model_name} created!"
+ end
+
+ def self.estimated_time_message(size)
+ estimated_minutes = (size.to_f / ESTIMATED_INSERT_PER_MINUTE).round
+ humanized_minutes = 'minute'.pluralize(estimated_minutes)
+
+ if estimated_minutes.zero?
+ "Rough estimated time: less than a minute ⏰"
+ else
+ "Rough estimated time: #{estimated_minutes} #{humanized_minutes} ⏰"
+ end
+ end
+
def self.quiet
+ # Disable database insertion logs so speed isn't limited by ability to print to console
+ old_logger = ActiveRecord::Base.logger
+ ActiveRecord::Base.logger = nil
+
+ # Additional seed logic for models.
+ Project.include(ProjectSeed)
+ User.include(UserSeed)
+
mute_notifications
mute_mailer
@@ -23,6 +87,7 @@ module Gitlab
yield
SeedFu.quiet = false
+ ActiveRecord::Base.logger = old_logger
puts "\nOK".color(:green)
end
diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb
index eb242cc7c20..bb7571dd66a 100644
--- a/lib/gitlab/serializer/pagination.rb
+++ b/lib/gitlab/serializer/pagination.rb
@@ -4,7 +4,6 @@ module Gitlab
module Serializer
class Pagination
InvalidResourceError = Class.new(StandardError)
- include ::API::Helpers::Pagination
def initialize(request, response)
@request = request
@@ -13,13 +12,13 @@ module Gitlab
def paginate(resource)
if resource.respond_to?(:page)
- super(resource)
+ ::Gitlab::Pagination::OffsetPagination.new(self).paginate(resource)
else
raise InvalidResourceError
end
end
- # Methods needed by `API::Helpers::Pagination`
+ # Methods needed by `Gitlab::Pagination::OffsetPagination`
#
attr_reader :request
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index 0d3e78c0a66..c449c6879bc 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -40,6 +40,11 @@ module Gitlab
config = { socket_path: address.sub(/\Aunix:/, ''), storage: storages }
config[:auth] = { token: 'secret' } if Rails.env.test?
+
+ internal_socket_dir = File.join(gitaly_dir, 'internal_sockets')
+ FileUtils.mkdir(internal_socket_dir) unless File.exist?(internal_socket_dir)
+ config[:internal_socket_dir] = internal_socket_dir
+
config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
config[:bin_dir] = Gitlab.config.gitaly.client_path
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 125d0d1cfbb..28e5d0ba8f5 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -285,18 +285,6 @@ module Gitlab
end
end
- # Check if such directory exists in repositories.
- #
- # Usage:
- # exists?(storage, 'gitlab')
- # exists?(storage, 'gitlab/cookies.git')
- #
- # rubocop: disable CodeReuse/ActiveRecord
- def exists?(storage, dir_name)
- Gitlab::GitalyClient::NamespaceService.new(storage).exists?(dir_name)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def repository_exists?(storage, dir_name)
Gitlab::Git::Repository.new(storage, dir_name, nil, nil).exists?
rescue GRPC::Internal
diff --git a/lib/gitlab/sidekiq_daemon/monitor.rb b/lib/gitlab/sidekiq_daemon/monitor.rb
index a3d61c69ae1..0723b514c90 100644
--- a/lib/gitlab/sidekiq_daemon/monitor.rb
+++ b/lib/gitlab/sidekiq_daemon/monitor.rb
@@ -4,6 +4,7 @@ module Gitlab
module SidekiqDaemon
class Monitor < Daemon
include ::Gitlab::Utils::StrongMemoize
+ extend ::Gitlab::Utils::Override
NOTIFICATION_CHANNEL = 'sidekiq:cancel:notifications'
CANCEL_DEADLINE = 24.hours.seconds
@@ -24,6 +25,11 @@ module Gitlab
@jobs_mutex = Mutex.new
end
+ override :thread_name
+ def thread_name
+ "job_monitor"
+ end
+
def within_job(worker_class, jid, queue)
jobs_mutex.synchronize do
jobs[jid] = { worker_class: worker_class, thread: Thread.current, started_at: Gitlab::Metrics::System.monotonic_time }
diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb
index 853fb2777c3..ca9e3b8428c 100644
--- a/lib/gitlab/sidekiq_logging/structured_logger.rb
+++ b/lib/gitlab/sidekiq_logging/structured_logger.rb
@@ -36,11 +36,8 @@ module Gitlab
payload['message'] = "#{base_message(payload)}: start"
payload['job_status'] = 'start'
- # Old gitlab-shell messages don't provide enqueued_at/created_at attributes
- enqueued_at = payload['enqueued_at'] || payload['created_at']
- if enqueued_at
- payload['scheduling_latency_s'] = elapsed_by_absolute_time(Time.iso8601(enqueued_at))
- end
+ scheduling_latency_s = ::Gitlab::InstrumentationHelper.queue_duration_for_job(payload)
+ payload['scheduling_latency_s'] = scheduling_latency_s if scheduling_latency_s
payload
end
@@ -98,10 +95,6 @@ module Gitlab
end
end
- def elapsed_by_absolute_time(start)
- (Time.now.utc - start).to_f.round(6)
- end
-
def elapsed(t0)
t1 = get_time
{
diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb
index 8af353d8674..bd819843bd4 100644
--- a/lib/gitlab/sidekiq_middleware/metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/metrics.rb
@@ -9,43 +9,56 @@ module Gitlab
def initialize
@metrics = init_metrics
+
+ @metrics[:sidekiq_concurrency].set({}, Sidekiq.options[:concurrency].to_i)
end
def call(_worker, job, queue)
labels = create_labels(queue)
+ queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job)
+
+ @metrics[:sidekiq_jobs_queue_duration_seconds].observe(labels, queue_duration) if queue_duration
@metrics[:sidekiq_running_jobs].increment(labels, 1)
if job['retry_count'].present?
@metrics[:sidekiq_jobs_retried_total].increment(labels, 1)
end
+ job_succeeded = false
+ monotonic_time_start = Gitlab::Metrics::System.monotonic_time
job_thread_cputime_start = get_thread_cputime
-
- realtime = Benchmark.realtime do
+ begin
yield
- end
+ job_succeeded = true
+ ensure
+ monotonic_time_end = Gitlab::Metrics::System.monotonic_time
+ job_thread_cputime_end = get_thread_cputime
+
+ monotonic_time = monotonic_time_end - monotonic_time_start
+ job_thread_cputime = job_thread_cputime_end - job_thread_cputime_start
- job_thread_cputime_end = get_thread_cputime
- job_thread_cputime = job_thread_cputime_end - job_thread_cputime_start
- @metrics[:sidekiq_jobs_cpu_seconds].observe(labels, job_thread_cputime)
+ # sidekiq_running_jobs, sidekiq_jobs_failed_total should not include the job_status label
+ @metrics[:sidekiq_running_jobs].increment(labels, -1)
+ @metrics[:sidekiq_jobs_failed_total].increment(labels, 1) unless job_succeeded
- @metrics[:sidekiq_jobs_completion_seconds].observe(labels, realtime)
- rescue Exception # rubocop: disable Lint/RescueException
- @metrics[:sidekiq_jobs_failed_total].increment(labels, 1)
- raise
- ensure
- @metrics[:sidekiq_running_jobs].increment(labels, -1)
+ # job_status: done, fail match the job_status attribute in structured logging
+ labels[:job_status] = job_succeeded ? :done : :fail
+ @metrics[:sidekiq_jobs_cpu_seconds].observe(labels, job_thread_cputime)
+ @metrics[:sidekiq_jobs_completion_seconds].observe(labels, monotonic_time)
+ end
end
private
def init_metrics
{
- sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds of cpu time to run sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'),
- sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
- sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :livesum)
+ sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds of cpu time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
+ sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
+ sidekiq_jobs_queue_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_queue_duration_seconds, 'Duration in seconds that a Sidekiq job was queued before being executed', {}, SIDEKIQ_LATENCY_BUCKETS),
+ sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'),
+ sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
+ sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :all),
+ sidekiq_concurrency: ::Gitlab::Metrics.gauge(:sidekiq_concurrency, 'Maximum number of Sidekiq jobs', {}, :all)
}
end
diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb
index 079b5916566..239479f99d2 100644
--- a/lib/gitlab/slash_commands/command.rb
+++ b/lib/gitlab/slash_commands/command.rb
@@ -10,6 +10,7 @@ module Gitlab
Gitlab::SlashCommands::IssueSearch,
Gitlab::SlashCommands::IssueMove,
Gitlab::SlashCommands::IssueClose,
+ Gitlab::SlashCommands::IssueComment,
Gitlab::SlashCommands::Deploy,
Gitlab::SlashCommands::Run
]
diff --git a/lib/gitlab/slash_commands/issue_comment.rb b/lib/gitlab/slash_commands/issue_comment.rb
new file mode 100644
index 00000000000..cbb9c41aab0
--- /dev/null
+++ b/lib/gitlab/slash_commands/issue_comment.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SlashCommands
+ class IssueComment < IssueCommand
+ def self.match(text)
+ /\Aissue\s+comment\s+#{Issue.reference_prefix}?(?<iid>\d+)\n*(?<note_body>(.|\n)*)/.match(text)
+ end
+
+ def self.help_message
+ 'issue comment <id> *`⇧ Shift`*+*`↵ Enter`* <comment>'
+ end
+
+ def execute(match)
+ note_body = match[:note_body].to_s.strip
+ issue = find_by_iid(match[:iid])
+
+ return not_found unless issue
+ return access_denied unless can_create_note?(issue)
+
+ note = create_note(issue: issue, note: note_body)
+
+ if note.persisted?
+ presenter(note).present
+ else
+ presenter(note).display_errors
+ end
+ end
+
+ private
+
+ def can_create_note?(issue)
+ Ability.allowed?(current_user, :create_note, issue)
+ end
+
+ def not_found
+ Gitlab::SlashCommands::Presenters::Access.new.not_found
+ end
+
+ def access_denied
+ Gitlab::SlashCommands::Presenters::Access.new.generic_access_denied
+ end
+
+ def create_note(issue:, note:)
+ note_params = { noteable: issue, note: note }
+
+ Notes::CreateService.new(project, current_user, note_params).execute
+ end
+
+ def presenter(note)
+ Gitlab::SlashCommands::Presenters::IssueComment.new(note)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/access.rb b/lib/gitlab/slash_commands/presenters/access.rb
index 9ce1bcfb37c..fbc3cf2e049 100644
--- a/lib/gitlab/slash_commands/presenters/access.rb
+++ b/lib/gitlab/slash_commands/presenters/access.rb
@@ -15,6 +15,10 @@ module Gitlab
MESSAGE
end
+ def generic_access_denied
+ ephemeral_response(text: 'You are not allowed to perform the given chatops command.')
+ end
+
def deactivated
ephemeral_response(text: <<~MESSAGE)
You are not allowed to perform the given chatops command since
diff --git a/lib/gitlab/slash_commands/presenters/issue_comment.rb b/lib/gitlab/slash_commands/presenters/issue_comment.rb
new file mode 100644
index 00000000000..cce71e23b21
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/issue_comment.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SlashCommands
+ module Presenters
+ class IssueComment < Presenters::Base
+ include Presenters::NoteBase
+
+ def present
+ ephemeral_response(new_note)
+ end
+
+ private
+
+ def new_note
+ {
+ attachments: [
+ {
+ title: "#{issue.title} · #{issue.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "New comment on #{issue.to_reference}: #{issue.title}",
+ pretext: pretext,
+ color: color,
+ fields: fields,
+ mrkdwn_in: [
+ :title,
+ :pretext,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def pretext
+ "I commented on an issue on #{author_profile_link}'s behalf: *#{issue.to_reference}* in #{project_link}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/note_base.rb b/lib/gitlab/slash_commands/presenters/note_base.rb
new file mode 100644
index 00000000000..7758fc740de
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/note_base.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SlashCommands
+ module Presenters
+ module NoteBase
+ GREEN = '#38ae67'
+
+ def color
+ GREEN
+ end
+
+ def issue
+ resource.noteable
+ end
+
+ def project
+ issue.project
+ end
+
+ def project_link
+ "[#{project.full_name}](#{project.web_url})"
+ end
+
+ def author
+ resource.author
+ end
+
+ def author_profile_link
+ "[#{author.to_reference}](#{url_for(author)})"
+ end
+
+ def fields
+ [
+ {
+ title: 'Comment',
+ value: resource.note
+ }
+ ]
+ end
+
+ private
+
+ attr_reader :resource
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sourcegraph.rb b/lib/gitlab/sourcegraph.rb
new file mode 100644
index 00000000000..d0f12c8364a
--- /dev/null
+++ b/lib/gitlab/sourcegraph.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class Sourcegraph
+ class << self
+ def feature_conditional?
+ feature.conditional?
+ end
+
+ def feature_available?
+ # The sourcegraph_bundle feature could be conditionally applied, so check if `!off?`
+ !feature.off?
+ end
+
+ def feature_enabled?(thing = nil)
+ feature.enabled?(thing)
+ end
+
+ private
+
+ def feature
+ Feature.get(:sourcegraph)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
index f05592fc3a3..b15f2ca385a 100644
--- a/lib/gitlab/sql/union.rb
+++ b/lib/gitlab/sql/union.rb
@@ -29,7 +29,7 @@ module Gitlab
end
if fragments.any?
- fragments.join("\n#{union_keyword}\n")
+ "(" + fragments.join(")\n#{union_keyword}\n(") + ")"
else
'NULL'
end
diff --git a/lib/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb
index 8532845f3cb..ac02ec635e4 100644
--- a/lib/gitlab/task_helpers.rb
+++ b/lib/gitlab/task_helpers.rb
@@ -158,15 +158,17 @@ module Gitlab
end
def checkout_or_clone_version(version:, repo:, target_dir:)
- version =
- if version.starts_with?("=")
- version.sub(/\A=/, '') # tag or branch
- else
- "v#{version}" # tag
- end
-
clone_repo(repo, target_dir) unless Dir.exist?(target_dir)
- checkout_version(version, target_dir)
+ checkout_version(get_version(version), target_dir)
+ end
+
+ # this function implements the same logic we have in omnibus for dealing with components version
+ def get_version(component_version)
+ # If not a valid version string following SemVer it is probably a branch name or a SHA
+ # commit of one of our own component so it doesn't need `v` prepended
+ return component_version unless /^\d+\.\d+\.\d+(-rc\d+)?$/.match?(component_version)
+
+ "v#{component_version}"
end
def clone_repo(repo, target_dir)
diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
index 2470685bc00..91e2ff0b10d 100644
--- a/lib/gitlab/tracking.rb
+++ b/lib/gitlab/tracking.rb
@@ -45,9 +45,10 @@ module Gitlab
namespace: SNOWPLOW_NAMESPACE,
hostname: Gitlab::CurrentSettings.snowplow_collector_hostname,
cookie_domain: Gitlab::CurrentSettings.snowplow_cookie_domain,
- app_id: Gitlab::CurrentSettings.snowplow_site_id,
+ app_id: Gitlab::CurrentSettings.snowplow_app_id,
form_tracking: additional_features,
- link_click_tracking: additional_features
+ link_click_tracking: additional_features,
+ iglu_registry_url: Gitlab::CurrentSettings.snowplow_iglu_registry_url
}.transform_keys! { |key| key.to_s.camelize(:lower).to_sym }
end
@@ -58,7 +59,7 @@ module Gitlab
SnowplowTracker::AsyncEmitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname, protocol: 'https'),
SnowplowTracker::Subject.new,
SNOWPLOW_NAMESPACE,
- Gitlab::CurrentSettings.snowplow_site_id
+ Gitlab::CurrentSettings.snowplow_app_id
)
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index cb492b69fec..b6effac25c6 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -13,7 +13,8 @@ module Gitlab
end
def uncached_data
- license_usage_data.merge(system_usage_data)
+ license_usage_data
+ .merge(system_usage_data)
.merge(features_usage_data)
.merge(components_usage_data)
.merge(cycle_analytics_usage_data)
@@ -66,17 +67,23 @@ module Gitlab
clusters_disabled: count(::Clusters::Cluster.disabled),
project_clusters_disabled: count(::Clusters::Cluster.disabled.project_type),
group_clusters_disabled: count(::Clusters::Cluster.disabled.group_type),
+ clusters_platforms_eks: count(::Clusters::Cluster.aws_installed.enabled),
clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled),
clusters_platforms_user: count(::Clusters::Cluster.user_provided.enabled),
clusters_applications_helm: count(::Clusters::Applications::Helm.available),
clusters_applications_ingress: count(::Clusters::Applications::Ingress.available),
clusters_applications_cert_managers: count(::Clusters::Applications::CertManager.available),
+ clusters_applications_crossplane: count(::Clusters::Applications::Crossplane.available),
clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.available),
clusters_applications_runner: count(::Clusters::Applications::Runner.available),
clusters_applications_knative: count(::Clusters::Applications::Knative.available),
+ clusters_applications_elastic_stack: count(::Clusters::Applications::ElasticStack.available),
in_review_folder: count(::Environment.in_review_folder),
+ grafana_integrated_projects: count(GrafanaIntegration.enabled),
groups: count(Group),
issues: count(Issue),
+ issues_with_associated_zoom_link: count(ZoomMeeting.added_to_issue),
+ issues_using_zoom_quick_actions: count(ZoomMeeting.select(:issue_id).distinct),
keys: count(Key),
label_lists: count(List.label),
lfs_objects: count(LfsObject),
@@ -127,7 +134,9 @@ module Gitlab
omniauth_enabled: Gitlab::Auth.omniauth_enabled?,
prometheus_metrics_enabled: Gitlab::Metrics.prometheus_metrics_enabled?,
reply_by_email_enabled: Gitlab::IncomingEmail.enabled?,
- signup_enabled: Gitlab::CurrentSettings.allow_signup?
+ signup_enabled: Gitlab::CurrentSettings.allow_signup?,
+ web_ide_clientside_preview_enabled: Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?,
+ ingress_modsecurity_enabled: Feature.enabled?(:ingress_modsecurity)
}
end
@@ -165,10 +174,13 @@ module Gitlab
types = {
SlackService: :projects_slack_notifications_active,
SlackSlashCommandsService: :projects_slack_slash_active,
- PrometheusService: :projects_prometheus_active
+ PrometheusService: :projects_prometheus_active,
+ CustomIssueTrackerService: :projects_custom_issue_tracker_active,
+ JenkinsService: :projects_jenkins_active,
+ MattermostService: :projects_mattermost_active
}
- results = count(Service.unscoped.where(type: types.keys, active: true).group(:type), fallback: Hash.new(-1))
+ results = count(Service.active.by_type(types.keys).group(:type), fallback: Hash.new(-1))
types.each_with_object({}) { |(klass, key), response| response[key] = results[klass.to_s] || 0 }
.merge(jira_usage)
end
@@ -183,8 +195,8 @@ module Gitlab
projects_jira_active: -1
}
- Service.unscoped
- .where(type: :JiraService, active: true)
+ Service.active
+ .by_type(:JiraService)
.includes(:jira_tracker_data)
.find_in_batches(batch_size: BATCH_SIZE) do |services|
diff --git a/lib/gitlab/usage_data_counters/web_ide_counter.rb b/lib/gitlab/usage_data_counters/web_ide_counter.rb
index 0718c1dd761..c012a6c96df 100644
--- a/lib/gitlab/usage_data_counters/web_ide_counter.rb
+++ b/lib/gitlab/usage_data_counters/web_ide_counter.rb
@@ -8,6 +8,7 @@ module Gitlab
COMMITS_COUNT_KEY = 'WEB_IDE_COMMITS_COUNT'
MERGE_REQUEST_COUNT_KEY = 'WEB_IDE_MERGE_REQUESTS_COUNT'
VIEWS_COUNT_KEY = 'WEB_IDE_VIEWS_COUNT'
+ PREVIEW_COUNT_KEY = 'WEB_IDE_PREVIEWS_COUNT'
class << self
def increment_commits_count
@@ -34,11 +35,22 @@ module Gitlab
total_count(VIEWS_COUNT_KEY)
end
+ def increment_previews_count
+ return unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
+
+ increment(PREVIEW_COUNT_KEY)
+ end
+
+ def total_previews_count
+ total_count(PREVIEW_COUNT_KEY)
+ end
+
def totals
{
web_ide_commits: total_commits_count,
web_ide_views: total_views_count,
- web_ide_merge_requests: total_merge_requests_count
+ web_ide_merge_requests: total_merge_requests_count,
+ web_ide_previews: total_previews_count
}
end
end
diff --git a/lib/gitlab/utils/deep_size.rb b/lib/gitlab/utils/deep_size.rb
index 562cf09e249..ed2ceb8af7c 100644
--- a/lib/gitlab/utils/deep_size.rb
+++ b/lib/gitlab/utils/deep_size.rb
@@ -25,6 +25,10 @@ module Gitlab
!too_big? && !too_deep?
end
+ def self.human_default_max_size
+ ActiveSupport::NumberHelper.number_to_human_size(DEFAULT_MAX_SIZE)
+ end
+
private
def evaluate
diff --git a/lib/gitlab/wiki_file_finder.rb b/lib/gitlab/wiki_file_finder.rb
index e9be6db50da..a963cc7954f 100644
--- a/lib/gitlab/wiki_file_finder.rb
+++ b/lib/gitlab/wiki_file_finder.rb
@@ -12,12 +12,12 @@ module Gitlab
private
- def search_filenames(query)
+ def search_paths(query)
safe_query = Regexp.escape(query.tr(' ', '-'))
safe_query = Regexp.new(safe_query, Regexp::IGNORECASE)
- filenames = repository.ls_files(ref)
+ paths = repository.ls_files(ref)
- filenames.grep(safe_query)
+ paths.grep(safe_query)
end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index db67e4fd479..713ca31bbc5 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -14,6 +14,7 @@ module Gitlab
NOTIFICATION_CHANNEL = 'workhorse:notifications'
ALLOWED_GIT_HTTP_ACTIONS = %w[git_receive_pack git_upload_pack info_refs].freeze
DETECT_HEADER = 'Gitlab-Workhorse-Detect-Content-Type'
+ ARCHIVE_FORMATS = %w(zip tar.gz tar.bz2 tar).freeze
include JwtAuthenticatable
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index 9085835dee6..99029b54a69 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -12,6 +12,7 @@ module GoogleApi
SCOPE = 'https://www.googleapis.com/auth/cloud-platform'
LEAST_TOKEN_LIFE_TIME = 10.minutes
CLUSTER_MASTER_AUTH_USERNAME = 'admin'
+ CLUSTER_IPV4_CIDR_BLOCK = '/16'
class << self
def session_key_for_token
@@ -97,7 +98,8 @@ module GoogleApi
enabled: legacy_abac
},
ip_allocation_policy: {
- use_ip_aliases: true
+ use_ip_aliases: true,
+ cluster_ipv4_cidr_block: CLUSTER_IPV4_CIDR_BLOCK
},
addons_config: enable_addons.each_with_object({}) do |addon, hash|
hash[addon] = { disabled: false }
diff --git a/lib/grafana/client.rb b/lib/grafana/client.rb
index 0765630f9bb..b419f79bace 100644
--- a/lib/grafana/client.rb
+++ b/lib/grafana/client.rb
@@ -11,6 +11,18 @@ module Grafana
@token = token
end
+ # @param uid [String] Unique identifier for a Grafana dashboard
+ def get_dashboard(uid:)
+ http_get("#{@api_url}/api/dashboards/uid/#{uid}")
+ end
+
+ # @param name [String] Unique identifier for a Grafana datasource
+ def get_datasource(name:)
+ # CGI#escape formats strings such that the Grafana endpoint
+ # will not recognize the dashboard name. Preferring URI#escape.
+ http_get("#{@api_url}/api/datasources/name/#{URI.escape(name)}") # rubocop:disable Lint/UriEscapeUnescape
+ end
+
# @param datasource_id [String] Grafana ID for the datasource
# @param proxy_path [String] Path to proxy - ex) 'api/v1/query_range'
def proxy_datasource(datasource_id:, proxy_path:, query: {})
@@ -57,7 +69,7 @@ module Grafana
def handle_response(response)
return response if response.code == 200
- raise_error "Grafana response status code: #{response.code}"
+ raise_error "Grafana response status code: #{response.code}, Message: #{response.body}"
end
def raise_error(message)
diff --git a/lib/prometheus/pid_provider.rb b/lib/prometheus/pid_provider.rb
index e0f7e7e0a9e..228639357ac 100644
--- a/lib/prometheus/pid_provider.rb
+++ b/lib/prometheus/pid_provider.rb
@@ -6,7 +6,7 @@ module Prometheus
def worker_id
if Sidekiq.server?
- 'sidekiq'
+ sidekiq_worker_id
elsif defined?(Unicorn::Worker)
unicorn_worker_id
elsif defined?(::Puma)
@@ -18,6 +18,14 @@ module Prometheus
private
+ def sidekiq_worker_id
+ if worker = ENV['SIDEKIQ_WORKER_ID']
+ "sidekiq_#{worker}"
+ else
+ 'sidekiq'
+ end
+ end
+
def unicorn_worker_id
if matches = process_name.match(/unicorn.*worker\[([0-9]+)\]/)
"unicorn_#{matches[1]}"
diff --git a/lib/quality/kubernetes_client.rb b/lib/quality/kubernetes_client.rb
index 190b48ba7cb..cc899bf9374 100644
--- a/lib/quality/kubernetes_client.rb
+++ b/lib/quality/kubernetes_client.rb
@@ -12,7 +12,16 @@ module Quality
@namespace = namespace
end
- def cleanup(release_name:)
+ def cleanup(release_name:, wait: true)
+ selector = case release_name
+ when String
+ %(-l release="#{release_name}")
+ when Array
+ %(-l 'release in (#{release_name.join(', ')})')
+ else
+ raise ArgumentError, 'release_name must be a string or an array'
+ end
+
command = [
%(--namespace "#{namespace}"),
'delete',
@@ -20,7 +29,8 @@ module Quality
'--now',
'--ignore-not-found',
'--include-uninitialized',
- %(-l release="#{release_name}")
+ %(--wait=#{wait}),
+ selector
]
run_command(command)
diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb
index 07cca1c8d1e..6191d69c870 100644
--- a/lib/sentry/client.rb
+++ b/lib/sentry/client.rb
@@ -4,6 +4,7 @@ module Sentry
class Client
Error = Class.new(StandardError)
MissingKeysError = Class.new(StandardError)
+ ResponseInvalidSizeError = Class.new(StandardError)
attr_accessor :url, :token
@@ -12,9 +13,23 @@ module Sentry
@token = token
end
+ def issue_details(issue_id:)
+ issue = get_issue(issue_id: issue_id)
+
+ map_to_detailed_error(issue)
+ end
+
+ def issue_latest_event(issue_id:)
+ latest_event = get_issue_latest_event(issue_id: issue_id)
+
+ map_to_event(latest_event)
+ end
+
def list_issues(issue_status:, limit:)
issues = get_issues(issue_status: issue_status, limit: limit)
+ validate_size(issues)
+
handle_mapping_exceptions do
map_to_errors(issues)
end
@@ -30,6 +45,12 @@ module Sentry
private
+ def validate_size(issues)
+ return if Gitlab::Utils::DeepSize.new(issues).valid?
+
+ raise Client::ResponseInvalidSizeError, "Sentry API response is too big. Limit is #{Gitlab::Utils::DeepSize.human_default_max_size}."
+ end
+
def handle_mapping_exceptions(&block)
yield
rescue KeyError => e
@@ -61,6 +82,14 @@ module Sentry
})
end
+ def get_issue(issue_id:)
+ http_get(issue_api_url(issue_id))
+ end
+
+ def get_issue_latest_event(issue_id:)
+ http_get(issue_latest_event_api_url(issue_id))
+ end
+
def get_projects
http_get(projects_api_url)
end
@@ -88,7 +117,7 @@ module Sentry
raise_error "Sentry response status code: #{response.code}"
end
- response
+ response.parsed_response
end
def raise_error(message)
@@ -102,6 +131,20 @@ module Sentry
projects_url
end
+ def issue_api_url(issue_id)
+ issue_url = URI(@url)
+ issue_url.path = "/api/0/issues/#{issue_id}/"
+
+ issue_url
+ end
+
+ def issue_latest_event_api_url(issue_id)
+ latest_event_url = URI(@url)
+ latest_event_url.path = "/api/0/issues/#{issue_id}/events/latest/"
+
+ latest_event_url
+ end
+
def issues_api_url
issues_url = URI(@url + '/issues/')
issues_url.path.squeeze!('/')
@@ -119,38 +162,87 @@ module Sentry
def issue_url(id)
issues_url = @url + "/issues/#{id}"
- issues_url = ErrorTracking::ProjectErrorTrackingSetting.extract_sentry_external_url(issues_url)
- uri = URI(issues_url)
+ parse_sentry_url(issues_url)
+ end
+
+ def project_url
+ parse_sentry_url(@url)
+ end
+
+ def parse_sentry_url(api_url)
+ url = ErrorTracking::ProjectErrorTrackingSetting.extract_sentry_external_url(api_url)
+
+ uri = URI(url)
uri.path.squeeze!('/')
+ # Remove trailing spaces
+ uri = uri.to_s.gsub(/\/\z/, '')
- uri.to_s
+ uri
end
- def map_to_error(issue)
- id = issue.fetch('id')
+ def map_to_event(event)
+ stack_trace = parse_stack_trace(event)
- count = issue.fetch('count', nil)
+ Gitlab::ErrorTracking::ErrorEvent.new(
+ issue_id: event.dig('groupID'),
+ date_received: event.dig('dateReceived'),
+ stack_trace_entries: stack_trace
+ )
+ end
- frequency = issue.dig('stats', '24h')
- message = issue.dig('metadata', 'value')
+ def parse_stack_trace(event)
+ exception_entry = event.dig('entries')&.detect { |h| h['type'] == 'exception' }
+ return unless exception_entry
- external_url = issue_url(id)
+ exception_values = exception_entry.dig('data', 'values')
+ stack_trace_entry = exception_values&.detect { |h| h['stacktrace'].present? }
+ return unless stack_trace_entry
+
+ stack_trace_entry.dig('stacktrace', 'frames')
+ end
+
+ def map_to_detailed_error(issue)
+ Gitlab::ErrorTracking::DetailedError.new(
+ id: issue.fetch('id'),
+ first_seen: issue.fetch('firstSeen', nil),
+ last_seen: issue.fetch('lastSeen', nil),
+ title: issue.fetch('title', nil),
+ type: issue.fetch('type', nil),
+ user_count: issue.fetch('userCount', nil),
+ count: issue.fetch('count', nil),
+ message: issue.dig('metadata', 'value'),
+ culprit: issue.fetch('culprit', nil),
+ external_url: issue_url(issue.fetch('id')),
+ external_base_url: project_url,
+ short_id: issue.fetch('shortId', nil),
+ status: issue.fetch('status', nil),
+ frequency: issue.dig('stats', '24h'),
+ project_id: issue.dig('project', 'id'),
+ project_name: issue.dig('project', 'name'),
+ project_slug: issue.dig('project', 'slug'),
+ first_release_last_commit: issue.dig('firstRelease', 'lastCommit'),
+ last_release_last_commit: issue.dig('lastRelease', 'lastCommit'),
+ first_release_short_version: issue.dig('firstRelease', 'shortVersion'),
+ last_release_short_version: issue.dig('lastRelease', 'shortVersion')
+ )
+ end
+ def map_to_error(issue)
Gitlab::ErrorTracking::Error.new(
- id: id,
+ id: issue.fetch('id'),
first_seen: issue.fetch('firstSeen', nil),
last_seen: issue.fetch('lastSeen', nil),
title: issue.fetch('title', nil),
type: issue.fetch('type', nil),
user_count: issue.fetch('userCount', nil),
- count: count,
- message: message,
+ count: issue.fetch('count', nil),
+ message: issue.dig('metadata', 'value'),
culprit: issue.fetch('culprit', nil),
- external_url: external_url,
+ external_url: issue_url(issue.fetch('id')),
short_id: issue.fetch('shortId', nil),
status: issue.fetch('status', nil),
- frequency: frequency,
+ frequency: issue.dig('stats', '24h'),
project_id: issue.dig('project', 'id'),
project_name: issue.dig('project', 'name'),
project_slug: issue.dig('project', 'slug')
diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake
index b1db4dc94a6..0488f26318a 100644
--- a/lib/tasks/dev.rake
+++ b/lib/tasks/dev.rake
@@ -5,6 +5,10 @@ namespace :dev do
task setup: :environment do
ENV['force'] = 'yes'
Rake::Task["gitlab:setup"].invoke
+
+ # Make sure DB statistics are up to date.
+ ActiveRecord::Base.connection.execute('ANALYZE')
+
Rake::Task["gitlab:shell:setup"].invoke
end
diff --git a/lib/tasks/gitlab/graphql.rake b/lib/tasks/gitlab/graphql.rake
index 902f22684ee..f8ce3cd46a8 100644
--- a/lib/tasks/gitlab/graphql.rake
+++ b/lib/tasks/gitlab/graphql.rake
@@ -2,10 +2,24 @@
return if Rails.env.production?
+require 'graphql/rake_task'
+
namespace :gitlab do
OUTPUT_DIR = Rails.root.join("doc/api/graphql/reference")
TEMPLATES_DIR = 'lib/gitlab/graphql/docs/templates/'
+ # Defines tasks for dumping the GraphQL schema:
+ # - gitlab:graphql:schema:dump
+ # - gitlab:graphql:schema:idl
+ # - gitlab:graphql:schema:json
+ GraphQL::RakeTask.new(
+ schema_name: 'GitlabSchema',
+ dependencies: [:environment],
+ directory: OUTPUT_DIR,
+ idl_outfile: "gitlab_schema.graphql",
+ json_outfile: "gitlab_schema.json"
+ )
+
namespace :graphql do
desc 'GitLab | Generate GraphQL docs'
task compile_docs: :environment do
@@ -25,11 +39,20 @@ namespace :gitlab do
if doc == renderer.contents
puts "GraphQL documentation is up to date"
else
- puts '#' * 10
- puts '#'
- puts '# GraphQL documentation is outdated! Please update it by running `bundle exec rake gitlab:graphql:compile_docs`.'
- puts '#'
- puts '#' * 10
+ format_output('GraphQL documentation is outdated! Please update it by running `bundle exec rake gitlab:graphql:compile_docs`.')
+ abort
+ end
+ end
+
+ desc 'GitLab | Check if GraphQL schemas are up to date'
+ task check_schema: :environment do
+ idl_doc = File.read(Rails.root.join(OUTPUT_DIR, 'gitlab_schema.graphql'))
+ json_doc = File.read(Rails.root.join(OUTPUT_DIR, 'gitlab_schema.json'))
+
+ if idl_doc == GitlabSchema.to_definition && json_doc == GitlabSchema.to_json
+ puts "GraphQL schema is up to date"
+ else
+ format_output('GraphQL schema is outdated! Please update it by running `bundle exec rake gitlab:graphql:schema:dump`.')
abort
end
end
@@ -42,3 +65,12 @@ def render_options
template: Rails.root.join(TEMPLATES_DIR, 'default.md.haml')
}
end
+
+def format_output(str)
+ heading = '#' * 10
+ puts heading
+ puts '#'
+ puts "# #{str}"
+ puts '#'
+ puts heading
+end
diff --git a/lib/tasks/gitlab/seed.rake b/lib/tasks/gitlab/seed.rake
index d76e38b73b5..d758280ba69 100644
--- a/lib/tasks/gitlab/seed.rake
+++ b/lib/tasks/gitlab/seed.rake
@@ -22,7 +22,7 @@ namespace :gitlab do
[project]
else
- Project.find_each
+ Project.not_mass_generated.find_each
end
projects.each do |project|
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index abd47f018f1..a592015963d 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -43,7 +43,7 @@ namespace :gitlab do
[
%w(bin/install) + repository_storage_paths_args,
- %w(bin/compile)
+ %w(make build)
].each do |cmd|
unless Kernel.system(*cmd)
raise "command failed: #{cmd.join(' ')}"
diff --git a/lib/tasks/gitlab/uploads/legacy.rake b/lib/tasks/gitlab/uploads/legacy.rake
index 2eeb694d341..74db0060b8d 100644
--- a/lib/tasks/gitlab/uploads/legacy.rake
+++ b/lib/tasks/gitlab/uploads/legacy.rake
@@ -15,7 +15,7 @@ namespace :gitlab do
batch_size = 5000
delay_interval = 5.minutes.to_i
- Upload.where(uploader: 'AttachmentUploader').each_batch(of: batch_size) do |relation, index|
+ Upload.where(uploader: 'AttachmentUploader', model_type: 'Note').each_batch(of: batch_size) do |relation, index|
start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
delay = index * delay_interval