summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-05 15:08:48 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-05 15:08:48 +0000
commit205b6baf2677879c35968d2b659225b58e8a1227 (patch)
tree10ed06185aae2f6ed6e7c61349a92acab605daca
parentf34077e88198da754b4efecd1ce1d996ce982286 (diff)
downloadgitlab-ce-205b6baf2677879c35968d2b659225b58e8a1227.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql4
-rw-r--r--app/controllers/clusters/clusters_controller.rb1
-rw-r--r--app/finders/groups/user_groups_finder.rb8
-rw-r--r--app/graphql/types/permission_types/group_enum.rb2
-rw-r--r--app/helpers/environments_helper.rb1
-rw-r--r--app/models/clusters/applications/elastic_stack.rb113
-rw-r--r--app/models/clusters/cluster.rb21
-rw-r--r--app/models/clusters/concerns/elasticsearch_client.rb38
-rw-r--r--app/models/clusters/integrations/elastic_stack.rb40
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/project_import_state.rb9
-rw-r--r--app/presenters/clusters/integration_presenter.rb2
-rw-r--r--app/services/clusters/integrations/create_service.rb2
-rw-r--r--app/views/admin/application_settings/_grafana.html.haml2
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb6
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb6
-rw-r--r--doc/.vale/gitlab/spelling-exceptions.txt1
-rw-r--r--doc/administration/audit_event_streaming.md49
-rw-r--r--doc/administration/troubleshooting/defcon.md38
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/ci/pipeline_editor/index.md2
-rw-r--r--doc/ci/troubleshooting.md23
-rw-r--r--doc/development/documentation/styleguide/index.md13
-rw-r--r--doc/development/fe_guide/vue.md72
-rw-r--r--doc/user/clusters/agent/troubleshooting.md21
-rw-r--r--lib/system_check/app/redis_version_check.rb2
-rw-r--r--spec/db/schema_spec.rb10
-rw-r--r--spec/factories/clusters/applications/helm.rb4
-rw-r--r--spec/factories/clusters/clusters.rb1
-rw-r--r--spec/factories/clusters/integrations/elastic_stack.rb12
-rw-r--r--spec/factories/import_states.rb4
-rw-r--r--spec/factories/projects.rb4
-rw-r--r--spec/finders/groups/user_groups_finder_spec.rb30
-rw-r--r--spec/frontend/fixtures/jobs.rb32
-rw-r--r--spec/frontend/jobs/components/table/cells/actions_cell_spec.js41
-rw-r--r--spec/frontend/jobs/components/table/cells/job_cell_spec.js33
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js17
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_spec.js10
-rw-r--r--spec/frontend/jobs/mock_data.js411
-rw-r--r--spec/graphql/resolvers/users/groups_resolver_spec.rb19
-rw-r--r--spec/helpers/environments_helper_spec.rb1
-rw-r--r--spec/models/clusters/applications/elastic_stack_spec.rb177
-rw-r--r--spec/models/clusters/cluster_spec.rb17
-rw-r--r--spec/models/clusters/integrations/elastic_stack_spec.rb19
-rw-r--r--spec/models/environment_spec.rb19
-rw-r--r--spec/models/project_import_state_spec.rb41
-rw-r--r--spec/requests/api/graphql/current_user/groups_query_spec.rb34
-rw-r--r--spec/requests/api/project_import_spec.rb10
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb23
-rw-r--r--spec/services/clusters/integrations/create_service_spec.rb1
-rw-r--r--spec/workers/concerns/gitlab/github_import/object_importer_spec.rb22
-rw-r--r--spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb32
-rw-r--r--spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb3
-rw-r--r--spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb4
-rw-r--r--spec/workers/gitlab/github_import/import_issue_worker_spec.rb3
-rw-r--r--spec/workers/gitlab/github_import/import_note_worker_spec.rb3
-rw-r--r--spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb3
59 files changed, 466 insertions, 1058 deletions
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index d430399ae9d..6ed573a91f5 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -340,7 +340,6 @@ Layout/LineLength:
- 'app/models/ci/unit_test.rb'
- 'app/models/clusters/agent.rb'
- 'app/models/clusters/applications/cert_manager.rb'
- - 'app/models/clusters/applications/elastic_stack.rb'
- 'app/models/clusters/applications/knative.rb'
- 'app/models/clusters/applications/prometheus.rb'
- 'app/models/clusters/cluster.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 9fe0c3003c7..3064e8338b7 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-e48e575179544159732c490280f4f40b8dbf43a5
+d92a2acbdcc9e20cac9e64692564556314f6e476
diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
index f3ca958b3ca..5b1032c6448 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
+++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
@@ -1,8 +1,8 @@
-query getJobs($fullPath: ID!, $after: String, $statuses: [CiJobStatus!]) {
+query getJobs($fullPath: ID!, $after: String, $first: Int = 30, $statuses: [CiJobStatus!]) {
project(fullPath: $fullPath) {
id
__typename
- jobs(after: $after, first: 30, statuses: $statuses) {
+ jobs(after: $after, first: $first, statuses: $statuses) {
count
pageInfo {
endCursor
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index a04fd09aa22..51150700860 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -50,7 +50,6 @@ class Clusters::ClustersController < Clusters::BaseController
def show
if params[:tab] == 'integrations'
@prometheus_integration = Clusters::IntegrationPresenter.new(@cluster.find_or_build_integration_prometheus)
- @elastic_stack_integration = Clusters::IntegrationPresenter.new(@cluster.find_or_build_integration_elastic_stack)
end
end
diff --git a/app/finders/groups/user_groups_finder.rb b/app/finders/groups/user_groups_finder.rb
index f4aed413867..90367638dcf 100644
--- a/app/finders/groups/user_groups_finder.rb
+++ b/app/finders/groups/user_groups_finder.rb
@@ -35,7 +35,7 @@ module Groups
attr_reader :current_user, :target_user, :params
def sort(items)
- items.order(path: :asc, id: :asc) # rubocop: disable CodeReuse/ActiveRecord
+ items.order(Group.arel_table[:path].asc, Group.arel_table[:id].asc) # rubocop: disable CodeReuse/ActiveRecord
end
def by_search(items)
@@ -47,6 +47,8 @@ module Groups
def by_permission_scope
if permission_scope_create_projects?
target_user.manageable_groups(include_groups_with_developer_maintainer_access: true)
+ elsif permission_scope_transfer_projects?
+ target_user.manageable_groups(include_groups_with_developer_maintainer_access: false)
else
target_user.groups
end
@@ -55,5 +57,9 @@ module Groups
def permission_scope_create_projects?
params[:permission_scope] == :create_projects
end
+
+ def permission_scope_transfer_projects?
+ params[:permission_scope] == :transfer_projects
+ end
end
end
diff --git a/app/graphql/types/permission_types/group_enum.rb b/app/graphql/types/permission_types/group_enum.rb
index cc4f5e9f1f0..8b0fee8898c 100644
--- a/app/graphql/types/permission_types/group_enum.rb
+++ b/app/graphql/types/permission_types/group_enum.rb
@@ -7,6 +7,8 @@ module Types
description 'User permission on groups'
value 'CREATE_PROJECTS', value: :create_projects, description: 'Groups where the user can create projects.'
+ value 'TRANSFER_PROJECTS', value: :transfer_projects,
+ description: 'Groups where the user can transfer projects to.'
end
end
end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 59d43c51db2..2623e32dbc8 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -36,7 +36,6 @@ module EnvironmentsHelper
"environment_name": environment.name,
"environments_path": api_v4_projects_environments_path(id: project.id),
"environment_id": environment.id,
- "cluster_applications_documentation_path" => help_page_path('user/clusters/integrations.md', anchor: 'elastic-stack-cluster-integration'),
"clusters_path": project_clusters_path(project, format: :json)
}
end
diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb
deleted file mode 100644
index 73c731aab1a..00000000000
--- a/app/models/clusters/applications/elastic_stack.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class ElasticStack < ApplicationRecord
- include ::Clusters::Concerns::ElasticsearchClient
-
- VERSION = '3.0.0'
-
- self.table_name = 'clusters_applications_elastic_stacks'
-
- include ::Clusters::Concerns::ApplicationCore
- include ::Clusters::Concerns::ApplicationStatus
- include ::Clusters::Concerns::ApplicationVersion
- include ::Clusters::Concerns::ApplicationData
-
- default_value_for :version, VERSION
-
- after_destroy do
- cluster&.find_or_build_integration_elastic_stack&.update(enabled: false, chart_version: nil)
- end
-
- state_machine :status do
- after_transition any => [:installed] do |application|
- application.cluster&.find_or_build_integration_elastic_stack&.update(enabled: true, chart_version: application.version)
- end
-
- after_transition any => [:uninstalled] do |application|
- application.cluster&.find_or_build_integration_elastic_stack&.update(enabled: false, chart_version: nil)
- end
- end
-
- def chart
- 'elastic-stack/elastic-stack'
- end
-
- def repository
- 'https://charts.gitlab.io'
- end
-
- def install_command
- helm_command_module::InstallCommand.new(
- name: 'elastic-stack',
- version: VERSION,
- rbac: cluster.platform_kubernetes_rbac?,
- chart: chart,
- repository: repository,
- files: files,
- preinstall: migrate_to_3_script,
- postinstall: post_install_script
- )
- end
-
- def uninstall_command
- helm_command_module::DeleteCommand.new(
- name: 'elastic-stack',
- rbac: cluster.platform_kubernetes_rbac?,
- files: files,
- postdelete: post_delete_script
- )
- end
-
- def files
- super.merge('wait-for-elasticsearch.sh': File.read("#{Rails.root}/vendor/elastic_stack/wait-for-elasticsearch.sh"))
- end
-
- def chart_above_v2?
- Gem::Version.new(version) >= Gem::Version.new('2.0.0')
- end
-
- def chart_above_v3?
- Gem::Version.new(version) >= Gem::Version.new('3.0.0')
- end
-
- private
-
- def service_name
- chart_above_v3? ? 'elastic-stack-elasticsearch-master' : 'elastic-stack-elasticsearch-client'
- end
-
- def pvc_selector
- chart_above_v3? ? "app=elastic-stack-elasticsearch-master" : "release=elastic-stack"
- end
-
- def post_install_script
- [
- "timeout 60 sh /data/helm/elastic-stack/config/wait-for-elasticsearch.sh http://elastic-stack-elasticsearch-master:9200"
- ]
- end
-
- def post_delete_script
- [
- Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", pvc_selector, "--namespace", Gitlab::Kubernetes::Helm::NAMESPACE)
- ]
- end
-
- def migrate_to_3_script
- return [] if !updating? || chart_above_v3?
-
- # Chart version 3.0.0 moves to our own chart at https://gitlab.com/gitlab-org/charts/elastic-stack
- # and is not compatible with pre-existing resources. We first remove them.
- [
- helm_command_module::DeleteCommand.new(
- name: 'elastic-stack',
- rbac: cluster.platform_kubernetes_rbac?,
- files: files
- ).delete_command,
- Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack", "--namespace", Gitlab::Kubernetes::Helm::NAMESPACE)
- ]
- end
- end
- end
-end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 014f7530357..ad1e7dc305f 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -20,7 +20,6 @@ module Clusters
Clusters::Applications::Runner.application_name => Clusters::Applications::Runner,
Clusters::Applications::Jupyter.application_name => Clusters::Applications::Jupyter,
Clusters::Applications::Knative.application_name => Clusters::Applications::Knative,
- Clusters::Applications::ElasticStack.application_name => Clusters::Applications::ElasticStack,
Clusters::Applications::Cilium.application_name => Clusters::Applications::Cilium
}.freeze
DEFAULT_ENVIRONMENT = '*'
@@ -51,7 +50,6 @@ module Clusters
has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true
has_one :integration_prometheus, class_name: 'Clusters::Integrations::Prometheus', inverse_of: :cluster
- has_one :integration_elastic_stack, class_name: 'Clusters::Integrations::ElasticStack', inverse_of: :cluster
def self.has_one_cluster_application(name) # rubocop:disable Naming/PredicateName
application = APPLICATIONS[name.to_s]
@@ -66,7 +64,6 @@ module Clusters
has_one_cluster_application :runner
has_one_cluster_application :jupyter
has_one_cluster_application :knative
- has_one_cluster_application :elastic_stack
has_one_cluster_application :cilium
has_many :kubernetes_namespaces
@@ -102,7 +99,6 @@ module Clusters
delegate :available?, to: :application_helm, prefix: true, allow_nil: true
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_knative, prefix: true, allow_nil: true
- delegate :available?, to: :integration_elastic_stack, prefix: true, allow_nil: true
delegate :available?, to: :integration_prometheus, prefix: true, allow_nil: true
delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true
delegate :external_hostname, to: :application_ingress, prefix: true, allow_nil: true
@@ -136,7 +132,6 @@ module Clusters
scope :gcp_installed, -> { gcp_provided.joins(:provider_gcp).merge(Clusters::Providers::Gcp.with_status(:created)) }
scope :aws_installed, -> { aws_provided.joins(:provider_aws).merge(Clusters::Providers::Aws.with_status(:created)) }
- scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) }
scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct }
scope :managed, -> { where(managed: true) }
@@ -271,10 +266,6 @@ module Clusters
integration_prometheus || build_integration_prometheus
end
- def find_or_build_integration_elastic_stack
- integration_elastic_stack || build_integration_elastic_stack
- end
-
def provider
if gcp?
provider_gcp
@@ -309,18 +300,6 @@ module Clusters
platform_kubernetes&.kubeclient if kubernetes?
end
- def elastic_stack_adapter
- integration_elastic_stack
- end
-
- def elasticsearch_client
- elastic_stack_adapter&.elasticsearch_client
- end
-
- def elastic_stack_available?
- !!integration_elastic_stack_available?
- end
-
def kubernetes_namespace_for(environment, deployable: environment.last_deployable)
if deployable && environment.project_id != deployable.project_id
raise ArgumentError, 'environment.project_id must match deployable.project_id'
diff --git a/app/models/clusters/concerns/elasticsearch_client.rb b/app/models/clusters/concerns/elasticsearch_client.rb
deleted file mode 100644
index e9aab7897a8..00000000000
--- a/app/models/clusters/concerns/elasticsearch_client.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Concerns
- module ElasticsearchClient
- include ::Gitlab::Utils::StrongMemoize
-
- ELASTICSEARCH_PORT = 9200
- ELASTICSEARCH_NAMESPACE = 'gitlab-managed-apps'
-
- def elasticsearch_client(timeout: nil)
- strong_memoize(:elasticsearch_client) do
- kube_client = cluster&.kubeclient&.core_client
- next unless kube_client
-
- proxy_url = kube_client.proxy_url('service', service_name, ELASTICSEARCH_PORT, ELASTICSEARCH_NAMESPACE)
-
- Elasticsearch::Client.new(url: proxy_url, adapter: :net_http) do |faraday|
- # ensures headers containing auth data are appended to original client options
- faraday.headers.merge!(kube_client.headers)
- # ensure TLS certs are properly verified
- faraday.ssl[:verify] = kube_client.ssl_options[:verify_ssl]
- faraday.ssl[:cert_store] = kube_client.ssl_options[:cert_store]
- faraday.options.timeout = timeout unless timeout.nil?
- end
-
- rescue Kubeclient::HttpError => error
- # If users have mistakenly set parameters or removed the depended clusters,
- # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
- # We check for a nil client in downstream use and behaviour is equivalent to an empty state
- log_exception(error, :failed_to_create_elasticsearch_client)
-
- nil
- end
- end
- end
- end
-end
diff --git a/app/models/clusters/integrations/elastic_stack.rb b/app/models/clusters/integrations/elastic_stack.rb
deleted file mode 100644
index 97d73d252b9..00000000000
--- a/app/models/clusters/integrations/elastic_stack.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Integrations
- class ElasticStack < ApplicationRecord
- include ::Clusters::Concerns::ElasticsearchClient
- include ::Clusters::Concerns::KubernetesLogger
-
- self.table_name = 'clusters_integration_elasticstack'
- self.primary_key = :cluster_id
-
- belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
-
- validates :cluster, presence: true
- validates :enabled, inclusion: { in: [true, false] }
-
- scope :enabled, -> { where(enabled: true) }
-
- def available?
- enabled
- end
-
- def service_name
- chart_above_v3? ? 'elastic-stack-elasticsearch-master' : 'elastic-stack-elasticsearch-client'
- end
-
- def chart_above_v2?
- return true if chart_version.nil?
-
- Gem::Version.new(chart_version) >= Gem::Version.new('2.0.0')
- end
-
- def chart_above_v3?
- return true if chart_version.nil?
-
- Gem::Version.new(chart_version) >= Gem::Version.new('3.0.0')
- end
- end
- end
-end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index da6ab5ed077..031a7f2fb83 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -456,10 +456,6 @@ class Environment < ApplicationRecord
self.auto_stop_at = parsed_result.seconds.from_now
end
- def elastic_stack_available?
- !!deployment_platform&.cluster&.elastic_stack_available?
- end
-
def rollout_status
return unless rollout_status_available?
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index b1c1a5b6697..7711c6d604a 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -31,6 +31,10 @@ class ProjectImportState < ApplicationRecord
transition started: :finished
end
+ event :cancel do
+ transition [:none, :scheduled, :started] => :canceled
+ end
+
event :fail_op do
transition [:scheduled, :started] => :failed
end
@@ -39,6 +43,7 @@ class ProjectImportState < ApplicationRecord
state :started
state :finished
state :failed
+ state :canceled
after_transition [:none, :finished, :failed] => :scheduled do |state, _|
state.run_after_commit do
@@ -51,7 +56,7 @@ class ProjectImportState < ApplicationRecord
end
end
- after_transition any => :finished do |state, _|
+ after_transition any => [:canceled, :finished] do |state, _|
if state.jid.present?
Gitlab::SidekiqStatus.unset(state.jid)
@@ -59,7 +64,7 @@ class ProjectImportState < ApplicationRecord
end
end
- after_transition any => :failed do |state, _|
+ after_transition any => [:canceled, :failed] do |state, _|
state.project.remove_import_data
end
diff --git a/app/presenters/clusters/integration_presenter.rb b/app/presenters/clusters/integration_presenter.rb
index f7be59f00f3..af735e1c18b 100644
--- a/app/presenters/clusters/integration_presenter.rb
+++ b/app/presenters/clusters/integration_presenter.rb
@@ -2,7 +2,7 @@
module Clusters
class IntegrationPresenter < Gitlab::View::Presenter::Delegated
- presents ::Clusters::Integrations::Prometheus, ::Clusters::Integrations::ElasticStack, as: :integration
+ presents ::Clusters::Integrations::Prometheus, as: :integration
def application_type
integration.class.name.demodulize.underscore
diff --git a/app/services/clusters/integrations/create_service.rb b/app/services/clusters/integrations/create_service.rb
index 142f731a7d3..555df52d177 100644
--- a/app/services/clusters/integrations/create_service.rb
+++ b/app/services/clusters/integrations/create_service.rb
@@ -31,8 +31,6 @@ module Clusters
case params[:application_type]
when 'prometheus'
cluster.find_or_build_integration_prometheus
- when 'elastic_stack'
- cluster.find_or_build_integration_elastic_stack
else
raise ArgumentError, "invalid application_type: #{params[:application_type]}"
end
diff --git a/app/views/admin/application_settings/_grafana.html.haml b/app/views/admin/application_settings/_grafana.html.haml
index 7f305b9ad9c..f17f63c7df7 100644
--- a/app/views/admin/application_settings/_grafana.html.haml
+++ b/app/views/admin/application_settings/_grafana.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-grafana-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index e1f404b250d..c2cd50d8c21 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -23,6 +23,12 @@ module Gitlab
# client - An instance of `Gitlab::GithubImport::Client`
# hash - A Hash containing the details of the object to import.
def import(project, client, hash)
+ if project.import_state&.canceled?
+ info(project.id, message: 'project import canceled')
+
+ return
+ end
+
object = representation_class.from_json_hash(hash)
# To better express in the logs what object is being imported.
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
index 225716f6bf3..b12c2311ea8 100644
--- a/app/workers/concerns/gitlab/github_import/stage_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -9,6 +9,12 @@ module Gitlab
return unless (project = find_project(project_id))
+ if project.import_state&.canceled?
+ info(project_id, message: 'project import canceled')
+
+ return
+ end
+
client = GithubImport.new_client_for(project)
try_import(client, project)
diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt
index de7fb45490d..09fb21f0e30 100644
--- a/doc/.vale/gitlab/spelling-exceptions.txt
+++ b/doc/.vale/gitlab/spelling-exceptions.txt
@@ -125,6 +125,7 @@ colocated
colocating
compilable
composable
+composables
Conda
Consul
Contentful
diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md
index 18070942813..1e87b3c65bf 100644
--- a/doc/administration/audit_event_streaming.md
+++ b/doc/administration/audit_event_streaming.md
@@ -506,3 +506,52 @@ X-Gitlab-Audit-Event-Type: audit_operation
"event_type": "audit_operation"
}
```
+
+## Audit event streaming on merge request create actions
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90911) in GitLab 15.2.
+
+Stream audit events that relate to merge request create actions using the `/logs` endpoint.
+
+Send API requests that contain the `X-Gitlab-Audit-Event-Type` header with value `merge_request_create`. GitLab responds with JSON payloads with an
+`event_type` field set to `merge_request_create`.
+
+### Headers
+
+Headers are formatted as follows:
+
+```plaintext
+POST /logs HTTP/1.1
+Host: <DESTINATION_HOST>
+Content-Type: application/x-www-form-urlencoded
+X-Gitlab-Audit-Event-Type: merge_request_create
+X-Gitlab-Event-Streaming-Token: <DESTINATION_TOKEN>
+```
+
+### Example payload
+
+```json
+{
+ "id": 1,
+ "author_id": 1,
+ "entity_id": 24,
+ "entity_type": "Project",
+ "details": {
+ "author_name": "example_user",
+ "target_id": 132,
+ "target_type": "MergeRequest",
+ "target_details": "Update test.md",
+ "custom_message": "Added merge request",
+ "ip_address": "127.0.0.1",
+ "entity_path": "example-group/example-project"
+ },
+ "ip_address": "127.0.0.1",
+ "author_name": "Administrator",
+ "entity_path": "example-group/example-project",
+ "target_details": "Update test.md",
+ "created_at": "2022-07-04T00:19:22.675Z",
+ "target_type": "MergeRequest",
+ "target_id": 132,
+ "event_type": "merge_request_create"
+}
+```
diff --git a/doc/administration/troubleshooting/defcon.md b/doc/administration/troubleshooting/defcon.md
index 292b4b13967..f2554f523f0 100644
--- a/doc/administration/troubleshooting/defcon.md
+++ b/doc/administration/troubleshooting/defcon.md
@@ -1,35 +1,11 @@
---
-stage: Systems
-group: Distribution
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
-type: reference
+redirect_to: '../../ci/troubleshooting.md#disaster-recovery'
+remove_date: '2022-10-04'
---
-# Disaster recovery **(FREE SELF)**
+This document was moved to [another location](../../ci/troubleshooting.md#disaster-recovery).
-This document describes a feature that allows you to disable some important but computationally
-expensive parts of the application to relieve stress on the database during an ongoing downtime.
-
-## `ci_queueing_disaster_recovery_disable_fair_scheduling`
-
-This feature flag, if temporarily enabled, disables fair scheduling on shared runners.
-This can help to reduce system resource usage on the `jobs/request` endpoint
-by significantly reducing the computations being performed.
-
-Side effects:
-
-- In case of a large backlog of jobs, the jobs are processed in the order
- they were put in the system, instead of balancing the jobs across many projects.
-
-## `ci_queueing_disaster_recovery_disable_quota`
-
-This feature flag, if temporarily enabled, disables enforcing CI/CD minutes quota
-on shared runners. This can help to reduce system resource usage on the
-`jobs/request` endpoint by significantly reducing the computations being
-performed.
-
-Side effects:
-
-- Projects which are out of quota will be run. This affects
- only jobs created during the last hour, as prior jobs are canceled
- by a periodic background worker (`StuckCiJobsWorker`).
+<!-- This redirect file can be deleted after <2022-10-04>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9b13c8303b0..4e62d0bf5d3 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -19245,6 +19245,7 @@ User permission on groups.
| Value | Description |
| ----- | ----------- |
| <a id="grouppermissioncreate_projects"></a>`CREATE_PROJECTS` | Groups where the user can create projects. |
+| <a id="grouppermissiontransfer_projects"></a>`TRANSFER_PROJECTS` | Groups where the user can transfer projects to. |
### `HealthStatus`
diff --git a/doc/ci/pipeline_editor/index.md b/doc/ci/pipeline_editor/index.md
index d87b336224c..4fb2ec94d60 100644
--- a/doc/ci/pipeline_editor/index.md
+++ b/doc/ci/pipeline_editor/index.md
@@ -98,7 +98,7 @@ where:
- [YAML `!reference` tags](../yaml/yaml_optimization.md#reference-tags) are also replaced
with the linked configuration.
-Using `!refence` tags can cause nested configuration that display with
+Using `!reference` tags can cause nested configuration that display with
multiple hyphens (`-`) in the expanded view. This behavior is expected, and the extra
hyphens do not affect the job's execution. For example, this configuration and
fully expanded version are both valid:
diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md
index 8d8afbffab9..0230aaf7113 100644
--- a/doc/ci/troubleshooting.md
+++ b/doc/ci/troubleshooting.md
@@ -361,6 +361,29 @@ When you visit the job log page for a running job, there could be a delay of up
60 seconds before the log updates. The default refresh time is 60 seconds, but after
the log is viewed in the UI, the following log updates should occur every 3 seconds.
+## Disaster recovery
+
+You can disable some important but computationally expensive parts of the application
+to relieve stress on the database during ongoing downtime.
+
+### Disable fair scheduling on shared runners
+
+When clearing a large backlog of jobs, you can temporarily enable the `ci_queueing_disaster_recovery_disable_fair_scheduling`
+[feature flag](../administration/feature_flags.md). This flag disables fair scheduling
+on shared runners, which reduces system resource usage on the `jobs/request` endpoint.
+
+When enabled, jobs are processed in the order they were put in the system, instead of
+balanced across many projects.
+
+### Disable CI/CD minutes quota enforcement
+
+To disable the enforcement of CI/CD minutes quotas on shared runners, you can temporarily
+enable the `ci_queueing_disaster_recovery_disable_quota` [feature flag](../administration/feature_flags.md).
+This flag reduces system resource usage on the `jobs/request` endpoint.
+
+When enabled, jobs created in the last hour can run in projects which are out of quota.
+Earlier jobs are already canceled by a periodic background worker (`StuckCiJobsWorker`).
+
## How to get help
If you are unable to resolve pipeline issues, you can get help from:
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 700d64c30d1..219b393afc4 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -1096,14 +1096,15 @@ copy of `https://gitlab.com/gitlab-org/gitlab`, run in a terminal:
### Animated images
-Sometimes an image with animation (such as an animated GIF)
-can help the reader understand a complicated interaction with the user interface.
+Avoid using animated images (such as animated GIFs). They can be distracting
+and annoying for users.
-However, you should use them sparingly and avoid them when you can.
-Do not use them to replace written descriptions of processes or the product.
+If you're describing a complicated interaction in the user interface and want to
+include a visual representation to help readers understand it, you can:
-If you include an animated image, follow the same size and naming conventions we use for images. If the animated image loops, add at least a three
-second pause to the end of the loop.
+- Use a static image (screenshot) and if necessary, add callouts to emphasize an
+ an area of the screen.
+- Create a short video of the interaction and link to it.
## Videos
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 4372e9203a1..7943ae119be 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -19,8 +19,8 @@ What is described in the following sections can be found in these examples:
## Vue architecture
All new features built with Vue.js must follow a [Flux architecture](https://facebook.github.io/flux/).
-The main goal we are trying to achieve is to have only one data flow and only one data entry.
-In order to achieve this goal we use [Vuex](#vuex).
+The main goal we are trying to achieve is to have only one data flow, and only one data entry.
+To achieve this goal we use [Vuex](#vuex).
You can also read about this architecture in Vue documentation about
[state management](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch)
@@ -48,8 +48,8 @@ Let's look into each of them:
### An `index.js` file
-This is the index file of your new feature. This is where the root Vue instance
-of the new feature should be.
+This file is the index file of your new feature. The root Vue instance
+of the new feature should be here.
The Store and the Service should be imported and initialized in this file and
provided as a prop to the main component.
@@ -62,17 +62,16 @@ Be sure to read about [page-specific JavaScript](performance.md#page-specific-ja
While mounting a Vue application, you might need to provide data from Rails to JavaScript.
To do that, you can use the `data` attributes in the HTML element and query them while mounting the application.
-
You should only do this while initializing the application, because the mounted element is replaced
with a Vue-generated DOM.
The advantage of providing data from the DOM to the Vue instance through `props` or
`provide` in the `render` function, instead of querying the DOM inside the main Vue
-component, is that you avoid the need to create a fixture or an HTML element in the unit test.
+component, is that you avoid creating a fixture or an HTML element in the unit test.
-##### provide/inject
+##### `provide` and `inject`
-Vue supports dependency injection through [provide/inject](https://vuejs.org/v2/api/#provide-inject).
+Vue supports dependency injection through [`provide` and `inject`](https://vuejs.org/v2/api/#provide-inject).
In the component the `inject` configuration accesses the values `provide` passes down.
This example of a Vue app initialization shows how the `provide` configuration passes a value from HAML to the component:
@@ -119,13 +118,16 @@ Using dependency injection to provide values from HAML is ideal when:
- The injected value doesn't need an explicit validation against its data type or contents.
- The value doesn't need to be reactive.
-- There are multiple components in the hierarchy that need access to this value where
+- Multiple components exist in the hierarchy that need access to this value where
prop-drilling becomes an inconvenience. Prop-drilling when the same prop is passed
through all components in the hierarchy until the component that is genuinely using it.
-Dependency injection can potentially break a child component (either an immediate child or multiple levels deep) if the value declared in the `inject` configuration doesn't have defaults defined and the parent component has not provided the value using the `provide` configuration.
+Dependency injection can potentially break a child component (either an immediate child or multiple levels deep) if both conditions are true:
+
+- The value declared in the `inject` configuration doesn't have defaults defined.
+- The parent component has not provided the value using the `provide` configuration.
-- A [default value](https://vuejs.org/guide/components/provide-inject.html#injection-default-values) might be useful in contexts where it makes sense.
+A [default value](https://vuejs.org/guide/components/provide-inject.html#injection-default-values) might be useful in contexts where it makes sense.
##### props
@@ -155,7 +157,8 @@ return new Vue({
});
```
-> When adding an `id` attribute to mount a Vue application, please make sure this `id` is unique
+NOTE:
+When adding an `id` attribute to mount a Vue application, make sure this `id` is unique
across the codebase.
For more information on why we explicitly declare the data being passed into the Vue app,
@@ -165,9 +168,9 @@ refer to our [Vue style guide](style/vue.md#basic-rules).
When composing a form with Rails, the `name`, `id`, and `value` attributes of form inputs are generated
to match the backend. It can be helpful to have access to these generated attributes when converting
-a Rails form to Vue, or when [integrating components (datepicker, project selector, etc)](https://gitlab.com/gitlab-org/gitlab/-/blob/8956ad767d522f37a96e03840595c767de030968/app/assets/javascripts/access_tokens/index.js#L15) into it.
+a Rails form to Vue, or when [integrating components](https://gitlab.com/gitlab-org/gitlab/-/blob/8956ad767d522f37a96e03840595c767de030968/app/assets/javascripts/access_tokens/index.js#L15) (such as a date picker or project selector) into it.
The [`parseRailsFormFields`](https://gitlab.com/gitlab-org/gitlab/-/blob/fe88797f682c7ff0b13f2c2223a3ff45ada751c1/app/assets/javascripts/lib/utils/forms.js#L107) utility can be used to parse the generated form input attributes so they can be passed to the Vue application.
-This allows us to easily integrate Vue components without changing how the form submits.
+This enables us to integrate Vue components without changing how the form submits.
```haml
-# form.html.haml
@@ -245,7 +248,7 @@ export default {
We query the `gl` object for data that doesn't change during the application's life
cycle in the same place we query the DOM. By following this practice, we can
-avoid the need to mock the `gl` object, which makes tests easier. It should be done while
+avoid mocking the `gl` object, which makes tests easier. It should be done while
initializing our Vue instance, and the data should be provided as `props` to the main component:
```javascript
@@ -263,8 +266,8 @@ return new Vue({
#### Accessing feature flags
-Use Vue's [provide/inject](https://vuejs.org/v2/api/#provide-inject) mechanism
-to make feature flags available to any descendant components in a Vue
+Use the [`provide` and `inject`](https://vuejs.org/v2/api/#provide-inject) mechanisms
+in Vue to make feature flags available to any descendant components in a Vue
application. The `glFeatures` object is already provided in `commons/vue.js`, so
only the mixin is required to use the flags:
@@ -303,14 +306,14 @@ This approach has a few benefits:
});
```
-- No need to access a global variable, except in the application's
+- Accessing a global variable is not required, except in the application's
[entry point](#accessing-the-gl-object).
### A folder for Components
This folder holds all components that are specific to this new feature.
-If you need to use or create a component that is likely to be used somewhere
-else, please refer to `vue_shared/components`.
+To use or create a component that is likely to be used somewhere
+else, refer to `vue_shared/components`.
A good guideline to know when you should create a component is to think if
it could be reusable elsewhere.
@@ -330,7 +333,7 @@ Check this [page](vuex.md) for more details.
### Mixing Vue and jQuery
- Mixing Vue and jQuery is not recommended.
-- If you need to use a specific jQuery plugin in Vue, [create a wrapper around it](https://vuejs.org/v2/examples/select2.html).
+- To use a specific jQuery plugin in Vue, [create a wrapper around it](https://vuejs.org/v2/examples/select2.html).
- It is acceptable for Vue to listen to existing jQuery events using jQuery event listeners.
- It is not recommended to add new jQuery events for Vue to interact with jQuery.
@@ -356,17 +359,17 @@ cannot use primitives or objects.
#### Why
-There are additional reasons why having a JavaScript class presents maintainability issues on a huge codebase:
+Additional reasons why having a JavaScript class presents maintainability issues on a huge codebase:
- After a class is created, it can be extended in a way that can infringe Vue reactivity and best practices.
- A class adds a layer of abstraction, which makes the component API and its inner workings less clear.
- It makes it harder to test. Because the class is instantiated by the component data function, it is
harder to 'manage' component and class separately.
-- Adding Object Oriented Principles (OOP) to a functional codebase adds yet another way of writing code, reducing consistency and clarity.
+- Adding Object Oriented Principles (OOP) to a functional codebase adds another way of writing code, reducing consistency and clarity.
## Style guide
-Please refer to the Vue section of our [style guide](style/vue.md)
+Refer to the Vue section of our [style guide](style/vue.md)
for best practices while writing and testing your Vue components and templates.
## Composition API
@@ -437,11 +440,11 @@ Common naming convention in Vue for composables is to prefix them with `use` and
#### Avoid lifecycle pitfalls
-When building a composable, we should aim to keep it as simple as possible. Lifecycle hooks add complexity to composables and might lead to unexpected side effects. In order to avoid that we should follow these principles:
+When building a composable, we should aim to keep it as simple as possible. Lifecycle hooks add complexity to composables and might lead to unexpected side effects. To avoid that we should follow these principles:
-- minimize lifecycle hooks usage whenever possible, prefer accepting/returning callbacks instead;
-- if you need to have lifecycle hooks in the composable, make sure this composable also performs a cleanup: if we are adding a listener on `onMounted`, we should remove it on `onUnmounted` within the same composable;
-- always set up lifecycle hooks immediately:
+- Minimize lifecycle hooks usage whenever possible, prefer accepting/returning callbacks instead.
+- If your composable needs lifecycle hooks, make sure it also performs a cleanup. If we add a listener on `onMounted`, we should remove it on `onUnmounted` within the same composable.
+- Always set up lifecycle hooks immediately:
```javascript
// bad
@@ -473,7 +476,7 @@ const useAsyncLogic = () => {
#### Avoid escape hatches
-It might be tempting to write a composable that does everything as a black box with a help of some of the escape hatches that Vue provides. But for most of the cases this makes them too complex and hard to maintain. One of these escape hatches is `getCurrentInstance` method which returns an instance of a current rendering component. Instead of using that method you should prefer passing down the data or methods to a composable via arguments.
+It might be tempting to write a composable that does everything as a black box, using some of the escape hatches that Vue provides. But for most of the cases this makes them too complex and hard to maintain. One escape hatch is the `getCurrentInstance` method. This method returns an instance of a current rendering component. Instead of using that method, you should prefer passing down the data or methods to a composable via arguments.
```javascript
const useSomeLogic = () => {
@@ -493,7 +496,7 @@ const useSomeLogic = (done) => {
#### Composables and Vuex
-We should always prefer to avoid using Vuex state in composables. In case it's not possible we should use props to receive that state and emit events from the `setup` to update the Vuex state. A parent component should be responsible to get that state from Vuex and mutate it on events emitted from a child. You should **never mutate a state that's coming down from a prop**. If a composable needs to mutate a Vuex state it should use a callback to emit an event.
+We should always prefer to avoid using Vuex state in composables. In case it's not possible, we should use props to receive that state, and emit events from the `setup` to update the Vuex state. A parent component should be responsible to get that state from Vuex, and mutate it on events emitted from a child. You should **never mutate a state that's coming down from a prop**. If a composable must mutate a Vuex state, it should use a callback to emit an event.
```javascript
const useAsyncComposable = ({ state, update }) => {
@@ -520,7 +523,7 @@ const ComponentWithComposable = {
## Testing Vue Components
-Please refer to the [Vue testing style guide](style/vue.md#vue-testing)
+Refer to the [Vue testing style guide](style/vue.md#vue-testing)
for guidelines and best practices for testing your Vue components.
Each Vue component has a unique output. This output is always present in the render function.
@@ -649,8 +652,8 @@ component under test, with the `computed` property, for example). Remember to us
### Events
-We should test for events emitted in response to an action in our component. This is used to
-verify the correct events are being fired with the correct arguments.
+We should test for events emitted in response to an action in our component. This testing
+verifies the correct events are being fired with the correct arguments.
For any DOM events we should use [`trigger`](https://v1.test-utils.vuejs.org/api/wrapper/#trigger)
to fire out event.
@@ -668,8 +671,7 @@ it('should fire the click event', () => {
})
```
-When we need to fire a Vue event, we should use [`emit`](https://vuejs.org/v2/guide/components-custom-events.html)
-to fire our event.
+When firing a Vue event, use [`emit`](https://vuejs.org/v2/guide/components-custom-events.html).
```javascript
wrapper = shallowMount(DropdownItem);
diff --git a/doc/user/clusters/agent/troubleshooting.md b/doc/user/clusters/agent/troubleshooting.md
index 14c09656bfa..0596755ec74 100644
--- a/doc/user/clusters/agent/troubleshooting.md
+++ b/doc/user/clusters/agent/troubleshooting.md
@@ -187,27 +187,6 @@ Alternatively, you can mount the certificate file at a different location and sp
This error occurs when the project where you keep your manifests is not public. To fix it, make sure your project is public or your manifest files
are stored in the repository where the agent is configured.
-## Failed to perform vulnerability scan on workload: Service account not found
-
-```json
-{
- "level": "error",
- "time": "2022-06-17T15:15:02.665Z",
- "msg": "Failed to perform vulnerability scan on workload",
- "mod_name": "starboard_vulnerability",
- "error": "getting service account by name: gitlab-agent/gitlab-agent: serviceaccounts \"gitlab-agent\" not found"
-}
-```
-
-The GitLab agent for Kubernetes has been able to run [vulnerability scans](vulnerabilities.md) since GitLab 15.0. However, the agent
-cannot detect the service account name. Refer to [issue 361972](https://gitlab.com/gitlab-org/gitlab/-/issues/361972) for more
-information. As a workaround you can pass the `--set serviceAccount.name=gitlab-agent` parameter
-to the Helm command when [installing the agent](install/#install-the-agent-in-the-cluster), or manually create a service account.
-
-```shell
-kubectl create serviceaccount gitlab-agent -n gitlab-agent
-```
-
## Failed to perform vulnerability scan on workload: jobs.batch already exists
```json
diff --git a/lib/system_check/app/redis_version_check.rb b/lib/system_check/app/redis_version_check.rb
index d907c041ad8..a6ff2405390 100644
--- a/lib/system_check/app/redis_version_check.rb
+++ b/lib/system_check/app/redis_version_check.rb
@@ -8,7 +8,7 @@ module SystemCheck
# Redis 5.x will be deprecated
# https://gitlab.com/gitlab-org/gitlab/-/issues/331468
MIN_REDIS_VERSION = '5.0.0'
- RECOMMENDED_REDIS_VERSION = '5.0.0'
+ RECOMMENDED_REDIS_VERSION = "6.0.0"
set_name "Redis version >= #{RECOMMENDED_REDIS_VERSION}?"
@custom_error_message = ''
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 8070e17b7af..bd13f86034a 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -114,11 +114,17 @@ RSpec.describe 'Database schema' do
context 'all foreign keys' do
# for index to be effective, the FK constraint has to be at first place
it 'are indexed' do
- first_indexed_column = indexes.map(&:columns).map do |columns|
+ first_indexed_column = indexes.filter_map do |index|
+ columns = index.columns
+
# In cases of complex composite indexes, a string is returned eg:
# "lower((extern_uid)::text), group_id"
columns = columns.split(',') if columns.is_a?(String)
- columns.first.chomp
+ column = columns.first.chomp
+
+ # A partial index is not suitable for a foreign key column, unless
+ # the only condition is for the presence of the foreign key itself
+ column if index.where.nil? || index.where == "(#{column} IS NOT NULL)"
end
foreign_keys_columns = all_foreign_keys.map(&:column)
required_indexed_columns = foreign_keys_columns - ignored_index_columns(table)
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 919b45e57e2..6a21df943f5 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -103,10 +103,6 @@ FactoryBot.define do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
- factory :clusters_applications_elastic_stack, class: 'Clusters::Applications::ElasticStack' do
- cluster factory: %i(cluster with_installed_helm provided_by_gcp)
- end
-
factory :clusters_applications_crossplane, class: 'Clusters::Applications::Crossplane' do
stack { 'gcp' }
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 7666533691e..72424a3c321 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -100,7 +100,6 @@ FactoryBot.define do
application_runner factory: %i(clusters_applications_runner installed)
application_jupyter factory: %i(clusters_applications_jupyter installed)
application_knative factory: %i(clusters_applications_knative installed)
- application_elastic_stack factory: %i(clusters_applications_elastic_stack installed)
application_cilium factory: %i(clusters_applications_cilium installed)
end
diff --git a/spec/factories/clusters/integrations/elastic_stack.rb b/spec/factories/clusters/integrations/elastic_stack.rb
deleted file mode 100644
index 1ab3256845b..00000000000
--- a/spec/factories/clusters/integrations/elastic_stack.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :clusters_integrations_elastic_stack, class: 'Clusters::Integrations::ElasticStack' do
- cluster factory: %i(cluster provided_by_gcp)
- enabled { true }
-
- trait :disabled do
- enabled { false }
- end
- end
-end
diff --git a/spec/factories/import_states.rb b/spec/factories/import_states.rb
index 4dca78b1059..0c73082be57 100644
--- a/spec/factories/import_states.rb
+++ b/spec/factories/import_states.rb
@@ -34,6 +34,10 @@ FactoryBot.define do
status { :failed }
end
+ trait :canceled do
+ status { :canceled }
+ end
+
after(:create) do |import_state, evaluator|
columns = {}
columns[:import_url] = evaluator.import_url unless evaluator.import_url.blank?
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 2ed71975d0c..76fa0c4c047 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -151,6 +151,10 @@ FactoryBot.define do
import_status { :failed }
end
+ trait :import_canceled do
+ import_status { :canceled }
+ end
+
trait :jira_dvcs_cloud do
before(:create) do |project|
create(:project_feature_usage, :dvcs_cloud, project: project)
diff --git a/spec/finders/groups/user_groups_finder_spec.rb b/spec/finders/groups/user_groups_finder_spec.rb
index a4a9b8d16d0..9339741da79 100644
--- a/spec/finders/groups/user_groups_finder_spec.rb
+++ b/spec/finders/groups/user_groups_finder_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Groups::UserGroupsFinder do
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') }
let_it_be(:public_developer_group) { create(:group, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
let_it_be(:public_maintainer_group) { create(:group, name: 'a public maintainer', path: 'a-public-maintainer') }
+ let_it_be(:public_owner_group) { create(:group, name: 'a public owner', path: 'a-public-owner') }
subject { described_class.new(current_user, target_user, arguments).execute }
@@ -21,12 +22,14 @@ RSpec.describe Groups::UserGroupsFinder do
private_maintainer_group.add_maintainer(user)
public_developer_group.add_developer(user)
public_maintainer_group.add_maintainer(user)
+ public_owner_group.add_owner(user)
end
it 'returns all groups where the user is a direct member' do
is_expected.to match(
[
public_maintainer_group,
+ public_owner_group,
private_maintainer_group,
public_developer_group,
guest_group
@@ -53,6 +56,7 @@ RSpec.describe Groups::UserGroupsFinder do
is_expected.to match(
[
public_maintainer_group,
+ public_owner_group,
private_maintainer_group,
public_developer_group
]
@@ -73,6 +77,32 @@ RSpec.describe Groups::UserGroupsFinder do
end
end
+ context 'when permission is :transfer_projects' do
+ let(:arguments) { { permission_scope: :transfer_projects } }
+
+ specify do
+ is_expected.to match(
+ [
+ public_maintainer_group,
+ public_owner_group,
+ private_maintainer_group
+ ]
+ )
+ end
+
+ context 'when search is provided' do
+ let(:arguments) { { permission_scope: :transfer_projects, search: 'owner' } }
+
+ specify do
+ is_expected.to match(
+ [
+ public_owner_group
+ ]
+ )
+ end
+ end
+ end
+
context 'when search is provided' do
let(:arguments) { { search: 'maintainer' } }
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index c76b06bd39e..2e15eefdce6 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -35,13 +35,21 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
end
describe GraphQL::Query, type: :request do
+ let(:artifact) { create(:ci_job_artifact, file_type: :archive, file_format: :zip) }
+
let!(:build) { create(:ci_build, :success, name: 'build', pipeline: pipeline) }
+ let!(:cancelable) { create(:ci_build, :cancelable, name: 'cancelable', pipeline: pipeline) }
let!(:created_by_tag) { create(:ci_build, :success, name: 'created_by_tag', tag: true, pipeline: pipeline) }
+ let!(:pending) { create(:ci_build, :pending, name: 'pending', pipeline: pipeline) }
+ let!(:playable) { create(:ci_build, :playable, name: 'playable', pipeline: pipeline) }
+ let!(:retryable) { create(:ci_build, :retryable, name: 'retryable', pipeline: pipeline) }
+ let!(:scheduled) { create(:ci_build, :scheduled, name: 'scheduled', pipeline: pipeline) }
+ let!(:with_artifact) { create(:ci_build, :success, name: 'with_artifact', job_artifacts: [artifact], pipeline: pipeline) }
let!(:with_coverage) { create(:ci_build, :success, name: 'with_coverage', coverage: 40.0, pipeline: pipeline) }
- let!(:stuck) { create(:ci_build, :pending, name: 'stuck', pipeline: pipeline) }
fixtures_path = 'graphql/jobs/'
get_jobs_query = 'get_jobs.query.graphql'
+ full_path = 'frontend-fixtures/builds-project'
let_it_be(:query) do
get_graphql_query_as_string("jobs/components/table/graphql/queries/#{get_jobs_query}")
@@ -49,7 +57,7 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
it "#{fixtures_path}#{get_jobs_query}.json" do
post_graphql(query, current_user: user, variables: {
- fullPath: 'frontend-fixtures/builds-project'
+ fullPath: full_path
})
expect_graphql_errors_to_be_empty
@@ -60,7 +68,25 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
project.add_guest(guest)
post_graphql(query, current_user: guest, variables: {
- fullPath: 'frontend-fixtures/builds-project'
+ fullPath: full_path
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "#{fixtures_path}#{get_jobs_query}.paginated.json" do
+ post_graphql(query, current_user: user, variables: {
+ fullPath: full_path,
+ first: 2
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "#{fixtures_path}#{get_jobs_query}.empty.json" do
+ post_graphql(query, current_user: user, variables: {
+ fullPath: full_path,
+ first: 0
})
expect_graphql_errors_to_be_empty
diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
index 976b128532d..7cc008f332d 100644
--- a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
@@ -12,17 +12,12 @@ import JobRetryMutation from '~/jobs/components/table/graphql/mutations/job_retr
import JobUnscheduleMutation from '~/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql';
import JobCancelMutation from '~/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql';
import {
- playableJob,
- retryableJob,
- cancelableJob,
- scheduledJob,
- cannotRetryJob,
- cannotPlayJob,
- cannotPlayScheduledJob,
- retryMutationResponse,
+ mockJobsNodes,
+ mockJobsNodesAsGuest,
playMutationResponse,
- cancelMutationResponse,
+ retryMutationResponse,
unscheduleMutationResponse,
+ cancelMutationResponse,
} from '../../../mock_data';
jest.mock('~/lib/utils/url_utility');
@@ -32,6 +27,22 @@ Vue.use(VueApollo);
describe('Job actions cell', () => {
let wrapper;
+ const findMockJob = (jobName, nodes = mockJobsNodes) => {
+ const job = nodes.find(({ name }) => name === jobName);
+ expect(job).toBeDefined(); // ensure job is present
+ return job;
+ };
+
+ const mockJob = findMockJob('build');
+ const cancelableJob = findMockJob('cancelable');
+ const playableJob = findMockJob('playable');
+ const retryableJob = findMockJob('retryable');
+ const scheduledJob = findMockJob('scheduled');
+ const jobWithArtifact = findMockJob('with_artifact');
+ const cannotPlayJob = findMockJob('playable', mockJobsNodesAsGuest);
+ const cannotRetryJob = findMockJob('retryable', mockJobsNodesAsGuest);
+ const cannotPlayScheduledJob = findMockJob('scheduled', mockJobsNodesAsGuest);
+
const findRetryButton = () => wrapper.findByTestId('retry');
const findPlayButton = () => wrapper.findByTestId('play');
const findCancelButton = () => wrapper.findByTestId('cancel-button');
@@ -55,10 +66,10 @@ describe('Job actions cell', () => {
return createMockApollo(requestHandlers);
};
- const createComponent = (jobType, requestHandlers, props = {}) => {
+ const createComponent = (job, requestHandlers, props = {}) => {
wrapper = shallowMountExtended(ActionsCell, {
propsData: {
- job: jobType,
+ job,
...props,
},
apolloProvider: createMockApolloProvider(requestHandlers),
@@ -73,15 +84,15 @@ describe('Job actions cell', () => {
});
it('displays the artifacts download button with correct link', () => {
- createComponent(playableJob);
+ createComponent(jobWithArtifact);
expect(findDownloadArtifactsButton().attributes('href')).toBe(
- playableJob.artifacts.nodes[0].downloadPath,
+ jobWithArtifact.artifacts.nodes[0].downloadPath,
);
});
it('does not display an artifacts download button', () => {
- createComponent(retryableJob);
+ createComponent(mockJob);
expect(findDownloadArtifactsButton().exists()).toBe(false);
});
@@ -101,7 +112,7 @@ describe('Job actions cell', () => {
button | action | jobType
${findPlayButton} | ${'play'} | ${playableJob}
${findRetryButton} | ${'retry'} | ${retryableJob}
- ${findDownloadArtifactsButton} | ${'download artifacts'} | ${playableJob}
+ ${findDownloadArtifactsButton} | ${'download artifacts'} | ${jobWithArtifact}
${findCancelButton} | ${'cancel'} | ${cancelableJob}
`('displays the $action button', ({ button, jobType }) => {
createComponent(jobType);
diff --git a/spec/frontend/jobs/components/table/cells/job_cell_spec.js b/spec/frontend/jobs/components/table/cells/job_cell_spec.js
index e3bef17b6fa..ddc196129a7 100644
--- a/spec/frontend/jobs/components/table/cells/job_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/job_cell_spec.js
@@ -2,13 +2,22 @@ import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import JobCell from '~/jobs/components/table/cells/job_cell.vue';
-import { mockJobsInTable, mockJobsAsGuestInTable } from '../../../mock_data';
-
-const getMockJob = (name) => mockJobsInTable.find((job) => job.name === name);
+import { mockJobsNodes, mockJobsNodesAsGuest } from '../../../mock_data';
describe('Job Cell', () => {
let wrapper;
+ const findMockJob = (jobName, nodes = mockJobsNodes) => {
+ const job = nodes.find(({ name }) => name === jobName);
+ expect(job).toBeDefined(); // ensure job is present
+ return job;
+ };
+
+ const mockJob = findMockJob('build');
+ const jobCreatedByTag = findMockJob('created_by_tag');
+ const pendingJob = findMockJob('pending');
+ const jobAsGuest = findMockJob('build', mockJobsNodesAsGuest);
+
const findJobIdLink = () => wrapper.findByTestId('job-id-link');
const findJobIdNoLink = () => wrapper.findByTestId('job-id-limited-access');
const findJobRef = () => wrapper.findByTestId('job-ref');
@@ -20,13 +29,11 @@ describe('Job Cell', () => {
const findBadgeById = (id) => wrapper.findByTestId(id);
- const mockJob = getMockJob('build');
-
- const createComponent = (jobData = mockJob) => {
+ const createComponent = (job = mockJob) => {
wrapper = extendedWrapper(
shallowMount(JobCell, {
propsData: {
- job: jobData,
+ job,
},
}),
);
@@ -48,11 +55,9 @@ describe('Job Cell', () => {
});
it('display the job id with no link', () => {
- const mockJobAsGuest = mockJobsAsGuestInTable[0];
-
- createComponent(mockJobAsGuest);
+ createComponent(jobAsGuest);
- const expectedJobId = `#${getIdFromGraphQLId(mockJobAsGuest.id)}`;
+ const expectedJobId = `#${getIdFromGraphQLId(jobAsGuest.id)}`;
expect(findJobIdNoLink().text()).toBe(expectedJobId);
expect(findJobIdNoLink().exists()).toBe(true);
@@ -76,7 +81,7 @@ describe('Job Cell', () => {
});
it('displays label icon when job is created by a tag', () => {
- createComponent(getMockJob('created_by_tag'));
+ createComponent(jobCreatedByTag);
expect(findLabelIcon().exists()).toBe(true);
expect(findForkIcon().exists()).toBe(false);
@@ -131,8 +136,8 @@ describe('Job Cell', () => {
expect(findStuckIcon().exists()).toBe(false);
});
- it('stuck icon is shown if job is stuck', () => {
- createComponent(getMockJob('stuck'));
+ it('stuck icon is shown if job is pending', () => {
+ createComponent(pendingJob);
expect(findStuckIcon().exists()).toBe(true);
expect(findStuckIcon().attributes('name')).toBe('warning');
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 986fba21fb9..963112fbd5e 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -18,8 +18,8 @@ import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
import {
- mockJobsQueryResponse,
- mockJobsQueryEmptyResponse,
+ mockJobsResponsePaginated,
+ mockJobsResponseEmpty,
mockFailedSearchToken,
} from '../../mock_data';
@@ -32,9 +32,9 @@ describe('Job table app', () => {
let wrapper;
let jobsTableVueSearch = true;
- const successHandler = jest.fn().mockResolvedValue(mockJobsQueryResponse);
+ const successHandler = jest.fn().mockResolvedValue(mockJobsResponsePaginated);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
- const emptyHandler = jest.fn().mockResolvedValue(mockJobsQueryEmptyResponse);
+ const emptyHandler = jest.fn().mockResolvedValue(mockJobsResponseEmpty);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
@@ -128,15 +128,18 @@ describe('Job table app', () => {
});
it('handles infinite scrolling by calling fetch more', async () => {
+ const pageSize = 30;
+
expect(findLoadingSpinner().exists()).toBe(true);
await waitForPromises();
expect(findLoadingSpinner().exists()).toBe(false);
- expect(successHandler).toHaveBeenCalledWith({
- after: 'eyJpZCI6IjIzMTcifQ',
- fullPath: 'gitlab-org/gitlab',
+ expect(successHandler).toHaveBeenLastCalledWith({
+ first: pageSize,
+ fullPath: projectPath,
+ after: mockJobsResponsePaginated.data.project.jobs.pageInfo.endCursor,
});
});
});
diff --git a/spec/frontend/jobs/components/table/jobs_table_spec.js b/spec/frontend/jobs/components/table/jobs_table_spec.js
index ac8bef675f8..803df3df37f 100644
--- a/spec/frontend/jobs/components/table/jobs_table_spec.js
+++ b/spec/frontend/jobs/components/table/jobs_table_spec.js
@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
-import { mockJobsInTable } from '../../mock_data';
+import { mockJobsNodes } from '../../mock_data';
describe('Jobs Table', () => {
let wrapper;
@@ -19,7 +19,7 @@ describe('Jobs Table', () => {
wrapper = extendedWrapper(
mount(JobsTable, {
propsData: {
- jobs: mockJobsInTable,
+ jobs: mockJobsNodes,
...props,
},
}),
@@ -39,7 +39,7 @@ describe('Jobs Table', () => {
});
it('displays correct number of job rows', () => {
- expect(findTableRows()).toHaveLength(mockJobsInTable.length);
+ expect(findTableRows()).toHaveLength(mockJobsNodes.length);
});
it('displays job status', () => {
@@ -47,14 +47,14 @@ describe('Jobs Table', () => {
});
it('displays the job stage and name', () => {
- const firstJob = mockJobsInTable[0];
+ const firstJob = mockJobsNodes[0];
expect(findJobStage().text()).toBe(firstJob.stage.name);
expect(findJobName().text()).toBe(firstJob.name);
});
it('displays the coverage for only jobs that have coverage', () => {
- const jobsThatHaveCoverage = mockJobsInTable.filter((job) => job.coverage !== null);
+ const jobsThatHaveCoverage = mockJobsNodes.filter((job) => job.coverage !== null);
jobsThatHaveCoverage.forEach((job, index) => {
expect(findAllCoverageJobs().at(index).text()).toBe(`${job.coverage}%`);
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index cc5fd92615a..4d7ea6a46bd 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1,3 +1,5 @@
+import mockJobsEmpty from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.empty.json';
+import mockJobsPaginated from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.paginated.json';
import mockJobs from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.json';
import mockJobsAsGuest from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.as_guest.json';
import { TEST_HOST } from 'spec/test_constants';
@@ -6,8 +8,10 @@ const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
// Fixtures generated at spec/frontend/fixtures/jobs.rb
-export const mockJobsInTable = mockJobs.data.project.jobs.nodes;
-export const mockJobsAsGuestInTable = mockJobsAsGuest.data.project.jobs.nodes;
+export const mockJobsResponsePaginated = mockJobsPaginated;
+export const mockJobsResponseEmpty = mockJobsEmpty;
+export const mockJobsNodes = mockJobs.data.project.jobs.nodes;
+export const mockJobsNodesAsGuest = mockJobsAsGuest.data.project.jobs.nodes;
export const stages = [
{
@@ -1289,409 +1293,6 @@ export const mockPipelineDetached = {
},
};
-export const mockJobsQueryResponse = {
- data: {
- project: {
- id: '1',
- jobs: {
- count: 1,
- pageInfo: {
- endCursor: 'eyJpZCI6IjIzMTcifQ',
- hasNextPage: true,
- hasPreviousPage: false,
- startCursor: 'eyJpZCI6IjIzMzYifQ',
- __typename: 'PageInfo',
- },
- nodes: [
- {
- artifacts: {
- nodes: [
- {
- downloadPath: '/root/ci-project/-/jobs/2336/artifacts/download?file_type=trace',
- fileType: 'TRACE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath:
- '/root/ci-project/-/jobs/2336/artifacts/download?file_type=metadata',
- fileType: 'METADATA',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath: '/root/ci-project/-/jobs/2336/artifacts/download?file_type=archive',
- fileType: 'ARCHIVE',
- __typename: 'CiJobArtifact',
- },
- ],
- __typename: 'CiJobArtifactConnection',
- },
- allowFailure: false,
- status: 'SUCCESS',
- scheduledAt: null,
- manualJob: false,
- triggered: null,
- createdByTag: false,
- detailedStatus: {
- id: 'status-1',
- detailsPath: '/root/ci-project/-/jobs/2336',
- group: 'success',
- icon: 'status_success',
- label: 'passed',
- text: 'passed',
- tooltip: 'passed',
- action: {
- id: 'action-1',
- buttonTitle: 'Retry this job',
- icon: 'retry',
- method: 'post',
- path: '/root/ci-project/-/jobs/2336/retry',
- title: 'Retry',
- __typename: 'StatusAction',
- },
- __typename: 'DetailedStatus',
- },
- id: 'gid://gitlab/Ci::Build/2336',
- refName: 'main',
- refPath: '/root/ci-project/-/commits/main',
- tags: [],
- shortSha: '4408fa2a',
- commitPath: '/root/ci-project/-/commit/4408fa2a27aaadfdf42d8dda3d6a9c01ce6cad78',
- pipeline: {
- id: 'gid://gitlab/Ci::Pipeline/473',
- path: '/root/ci-project/-/pipelines/473',
- user: {
- id: 'user-1',
- webPath: '/root',
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- __typename: 'UserCore',
- },
- __typename: 'Pipeline',
- },
- stage: {
- id: 'stage-1',
- name: 'deploy',
- __typename: 'CiStage',
- },
- name: 'artifact_job',
- duration: 3,
- finishedAt: '2021-04-29T14:19:50Z',
- coverage: null,
- retryable: true,
- playable: false,
- cancelable: false,
- active: false,
- stuck: false,
- userPermissions: {
- readBuild: true,
- readJobArtifacts: true,
- updateBuild: true,
- __typename: 'JobPermissions',
- },
- __typename: 'CiJob',
- },
- ],
- __typename: 'CiJobConnection',
- },
- __typename: 'Project',
- },
- },
-};
-
-export const mockJobsQueryEmptyResponse = {
- data: {
- project: {
- id: '1',
- jobs: [],
- },
- },
-};
-
-export const retryableJob = {
- artifacts: {
- nodes: [
- {
- downloadPath: '/root/ci-project/-/jobs/847/artifacts/download?file_type=trace',
- fileType: 'TRACE',
- __typename: 'CiJobArtifact',
- },
- ],
- __typename: 'CiJobArtifactConnection',
- },
- allowFailure: false,
- status: 'SUCCESS',
- scheduledAt: null,
- manualJob: false,
- triggered: null,
- createdByTag: false,
- detailedStatus: {
- detailsPath: '/root/test-job-artifacts/-/jobs/1981',
- group: 'success',
- icon: 'status_success',
- label: 'passed',
- text: 'passed',
- tooltip: 'passed',
- action: {
- buttonTitle: 'Retry this job',
- icon: 'retry',
- method: 'post',
- path: '/root/test-job-artifacts/-/jobs/1981/retry',
- title: 'Retry',
- __typename: 'StatusAction',
- },
- __typename: 'DetailedStatus',
- },
- id: 'gid://gitlab/Ci::Build/1981',
- refName: 'main',
- refPath: '/root/test-job-artifacts/-/commits/main',
- tags: [],
- shortSha: '75daf01b',
- commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055',
- pipeline: {
- id: 'gid://gitlab/Ci::Pipeline/288',
- path: '/root/test-job-artifacts/-/pipelines/288',
- user: {
- webPath: '/root',
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- __typename: 'UserCore',
- },
- __typename: 'Pipeline',
- },
- stage: { name: 'test', __typename: 'CiStage' },
- name: 'hello_world',
- duration: 7,
- finishedAt: '2021-08-30T20:33:56Z',
- coverage: null,
- retryable: true,
- playable: false,
- cancelable: false,
- active: false,
- stuck: false,
- userPermissions: { readBuild: true, updateBuild: true, __typename: 'JobPermissions' },
- __typename: 'CiJob',
-};
-
-export const cancelableJob = {
- artifacts: {
- nodes: [],
- __typename: 'CiJobArtifactConnection',
- },
- allowFailure: false,
- status: 'PENDING',
- scheduledAt: null,
- manualJob: false,
- triggered: null,
- createdByTag: false,
- detailedStatus: {
- id: 'pending-1305-1305',
- detailsPath: '/root/lots-of-jobs-project/-/jobs/1305',
- group: 'pending',
- icon: 'status_pending',
- label: 'pending',
- text: 'pending',
- tooltip: 'pending',
- action: {
- id: 'Ci::Build-pending-1305',
- buttonTitle: 'Cancel this job',
- icon: 'cancel',
- method: 'post',
- path: '/root/lots-of-jobs-project/-/jobs/1305/cancel',
- title: 'Cancel',
- __typename: 'StatusAction',
- },
- __typename: 'DetailedStatus',
- },
- id: 'gid://gitlab/Ci::Build/1305',
- refName: 'main',
- refPath: '/root/lots-of-jobs-project/-/commits/main',
- tags: [],
- shortSha: '750605f2',
- commitPath: '/root/lots-of-jobs-project/-/commit/750605f29530778cf0912779eba6d073128962a5',
- stage: {
- id: 'gid://gitlab/Ci::Stage/181',
- name: 'deploy',
- __typename: 'CiStage',
- },
- name: 'job_212',
- duration: null,
- finishedAt: null,
- coverage: null,
- retryable: false,
- playable: false,
- cancelable: true,
- active: true,
- stuck: false,
- userPermissions: {
- readBuild: true,
- readJobArtifacts: true,
- updateBuild: true,
- __typename: 'JobPermissions',
- },
- __typename: 'CiJob',
-};
-
-export const cannotRetryJob = {
- ...retryableJob,
- userPermissions: { readBuild: true, updateBuild: false, __typename: 'JobPermissions' },
-};
-
-export const playableJob = {
- artifacts: {
- nodes: [
- {
- downloadPath: '/root/ci-project/-/jobs/621/artifacts/download?file_type=archive',
- fileType: 'ARCHIVE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath: '/root/ci-project/-/jobs/621/artifacts/download?file_type=metadata',
- fileType: 'METADATA',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath: '/root/ci-project/-/jobs/621/artifacts/download?file_type=trace',
- fileType: 'TRACE',
- __typename: 'CiJobArtifact',
- },
- ],
- __typename: 'CiJobArtifactConnection',
- },
- allowFailure: false,
- status: 'SUCCESS',
- scheduledAt: null,
- manualJob: true,
- triggered: null,
- createdByTag: false,
- detailedStatus: {
- detailsPath: '/root/test-job-artifacts/-/jobs/1982',
- group: 'success',
- icon: 'status_success',
- label: 'manual play action',
- text: 'passed',
- tooltip: 'passed',
- action: {
- buttonTitle: 'Trigger this manual action',
- icon: 'play',
- method: 'post',
- path: '/root/test-job-artifacts/-/jobs/1982/play',
- title: 'Play',
- __typename: 'StatusAction',
- },
- __typename: 'DetailedStatus',
- },
- id: 'gid://gitlab/Ci::Build/1982',
- refName: 'main',
- refPath: '/root/test-job-artifacts/-/commits/main',
- tags: [],
- shortSha: '75daf01b',
- commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055',
- pipeline: {
- id: 'gid://gitlab/Ci::Pipeline/288',
- path: '/root/test-job-artifacts/-/pipelines/288',
- user: {
- webPath: '/root',
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- __typename: 'UserCore',
- },
- __typename: 'Pipeline',
- },
- stage: { name: 'test', __typename: 'CiStage' },
- name: 'hello_world_delayed',
- duration: 6,
- finishedAt: '2021-08-30T20:36:12Z',
- coverage: null,
- retryable: true,
- playable: true,
- cancelable: false,
- active: false,
- stuck: false,
- userPermissions: {
- readBuild: true,
- readJobArtifacts: true,
- updateBuild: true,
- __typename: 'JobPermissions',
- },
- __typename: 'CiJob',
-};
-
-export const cannotPlayJob = {
- ...playableJob,
- userPermissions: {
- readBuild: true,
- readJobArtifacts: true,
- updateBuild: false,
- __typename: 'JobPermissions',
- },
-};
-
-export const scheduledJob = {
- artifacts: { nodes: [], __typename: 'CiJobArtifactConnection' },
- allowFailure: false,
- status: 'SCHEDULED',
- scheduledAt: '2021-08-31T22:36:05Z',
- manualJob: true,
- triggered: null,
- createdByTag: false,
- detailedStatus: {
- detailsPath: '/root/test-job-artifacts/-/jobs/1986',
- group: 'scheduled',
- icon: 'status_scheduled',
- label: 'unschedule action',
- text: 'delayed',
- tooltip: 'delayed manual action (%{remainingTime})',
- action: {
- buttonTitle: 'Unschedule job',
- icon: 'time-out',
- method: 'post',
- path: '/root/test-job-artifacts/-/jobs/1986/unschedule',
- title: 'Unschedule',
- __typename: 'StatusAction',
- },
- __typename: 'DetailedStatus',
- },
- id: 'gid://gitlab/Ci::Build/1986',
- refName: 'main',
- refPath: '/root/test-job-artifacts/-/commits/main',
- tags: [],
- shortSha: '75daf01b',
- commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055',
- pipeline: {
- id: 'gid://gitlab/Ci::Pipeline/290',
- path: '/root/test-job-artifacts/-/pipelines/290',
- user: {
- webPath: '/root',
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- __typename: 'UserCore',
- },
- __typename: 'Pipeline',
- },
- stage: { name: 'test', __typename: 'CiStage' },
- name: 'hello_world_delayed',
- duration: null,
- finishedAt: null,
- coverage: null,
- retryable: false,
- playable: true,
- cancelable: false,
- active: false,
- stuck: false,
- userPermissions: { readBuild: true, updateBuild: true, __typename: 'JobPermissions' },
- __typename: 'CiJob',
-};
-
-export const cannotPlayScheduledJob = {
- ...scheduledJob,
- userPermissions: {
- readBuild: true,
- readJobArtifacts: true,
- updateBuild: false,
- __typename: 'JobPermissions',
- },
-};
-
export const CIJobConnectionIncomingCache = {
__typename: 'CiJobConnection',
pageInfo: {
diff --git a/spec/graphql/resolvers/users/groups_resolver_spec.rb b/spec/graphql/resolvers/users/groups_resolver_spec.rb
index bbe9b6371cf..1e0e001fbf7 100644
--- a/spec/graphql/resolvers/users/groups_resolver_spec.rb
+++ b/spec/graphql/resolvers/users/groups_resolver_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe Resolvers::Users::GroupsResolver do
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') }
let_it_be(:public_developer_group) { create(:group, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
let_it_be(:public_maintainer_group) { create(:group, name: 'a public maintainer', path: 'a-public-maintainer') }
+ let_it_be(:public_owner_group) { create(:group, name: 'a public owner', path: 'a-public-owner') }
subject(:resolved_items) { resolve_groups(args: group_arguments, current_user: current_user, obj: resolver_object) }
@@ -24,6 +25,7 @@ RSpec.describe Resolvers::Users::GroupsResolver do
private_maintainer_group.add_maintainer(user)
public_developer_group.add_developer(user)
public_maintainer_group.add_maintainer(user)
+ public_owner_group.add_owner(user)
end
context 'when resolver object is current user' do
@@ -34,6 +36,7 @@ RSpec.describe Resolvers::Users::GroupsResolver do
is_expected.to match(
[
public_maintainer_group,
+ public_owner_group,
private_maintainer_group,
public_developer_group
]
@@ -41,10 +44,25 @@ RSpec.describe Resolvers::Users::GroupsResolver do
end
end
+ context 'when permission is :transfer_projects' do
+ let(:group_arguments) { { permission_scope: :transfer_projects } }
+
+ specify do
+ is_expected.to match(
+ [
+ public_maintainer_group,
+ public_owner_group,
+ private_maintainer_group
+ ]
+ )
+ end
+ end
+
specify do
is_expected.to match(
[
public_maintainer_group,
+ public_owner_group,
private_maintainer_group,
public_developer_group,
guest_group
@@ -82,6 +100,7 @@ RSpec.describe Resolvers::Users::GroupsResolver do
is_expected.to match(
[
public_maintainer_group,
+ public_owner_group,
private_maintainer_group,
public_developer_group,
guest_group
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index e4d4f18ad68..c1eaf1b1bcd 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -129,7 +129,6 @@ RSpec.describe EnvironmentsHelper do
"environment_name": environment.name,
"environments_path": api_v4_projects_environments_path(id: project.id),
"environment_id": environment.id,
- "cluster_applications_documentation_path" => help_page_path('user/clusters/integrations.md', anchor: 'elastic-stack-cluster-integration'),
"clusters_path": project_clusters_path(project, format: :json)
}
diff --git a/spec/models/clusters/applications/elastic_stack_spec.rb b/spec/models/clusters/applications/elastic_stack_spec.rb
deleted file mode 100644
index af2802d5e47..00000000000
--- a/spec/models/clusters/applications/elastic_stack_spec.rb
+++ /dev/null
@@ -1,177 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::ElasticStack do
- include KubernetesHelpers
-
- include_examples 'cluster application core specs', :clusters_applications_elastic_stack
- include_examples 'cluster application status specs', :clusters_applications_elastic_stack
- include_examples 'cluster application version specs', :clusters_applications_elastic_stack
- include_examples 'cluster application helm specs', :clusters_applications_elastic_stack
-
- describe 'cluster.integration_elastic_stack state synchronization' do
- let!(:application) { create(:clusters_applications_elastic_stack) }
- let(:cluster) { application.cluster }
- let(:integration) { cluster.integration_elastic_stack }
-
- describe 'after_destroy' do
- it 'disables the corresponding integration' do
- application.destroy!
-
- expect(integration).not_to be_enabled
- end
- end
-
- describe 'on install' do
- it 'enables the corresponding integration' do
- application.make_scheduled!
- application.make_installing!
- application.make_installed!
-
- expect(integration).to be_enabled
- end
- end
-
- describe 'on uninstall' do
- it 'disables the corresponding integration' do
- application.make_scheduled!
- application.make_installing!
- application.make_installed!
- application.make_externally_uninstalled!
-
- expect(integration).not_to be_enabled
- end
- end
- end
-
- describe '#install_command' do
- let!(:elastic_stack) { create(:clusters_applications_elastic_stack) }
-
- subject { elastic_stack.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) }
-
- it 'is initialized with elastic stack arguments' do
- expect(subject.name).to eq('elastic-stack')
- expect(subject.chart).to eq('elastic-stack/elastic-stack')
- expect(subject.version).to eq('3.0.0')
- expect(subject.repository).to eq('https://charts.gitlab.io')
- expect(subject).to be_rbac
- expect(subject.files).to eq(elastic_stack.files)
- expect(subject.preinstall).to be_empty
- end
-
- context 'within values.yaml' do
- let(:values_yaml_content) {subject.files[:"values.yaml"]}
-
- it 'contains the disabled index lifecycle management' do
- expect(values_yaml_content).to include "setup.ilm.enabled: false"
- end
-
- it 'contains daily indices with respective template' do
- expect(values_yaml_content).to include "index: \"filebeat-%{[agent.version]}-%{+yyyy.MM.dd}\""
- expect(values_yaml_content).to include "setup.template.name: 'filebeat'"
- expect(values_yaml_content).to include "setup.template.pattern: 'filebeat-*'"
- end
- end
-
- context 'on a non rbac enabled cluster' do
- before do
- elastic_stack.cluster.platform_kubernetes.abac!
- end
-
- it { is_expected.not_to be_rbac }
- end
-
- context 'on versions older than 2' do
- before do
- elastic_stack.status = elastic_stack.status_states[:updating]
- elastic_stack.version = "1.9.0"
- end
-
- it 'includes a preinstall script' do
- expect(subject.preinstall).not_to be_empty
- expect(subject.preinstall.first).to include("helm uninstall")
- end
- end
-
- context 'on versions older than 3' do
- before do
- elastic_stack.status = elastic_stack.status_states[:updating]
- elastic_stack.version = "2.9.0"
- end
-
- it 'includes a preinstall script' do
- expect(subject.preinstall).not_to be_empty
- expect(subject.preinstall.first).to include("helm uninstall")
- end
- end
-
- context 'application failed to install previously' do
- let(:elastic_stack) { create(:clusters_applications_elastic_stack, :errored, version: '0.0.1') }
-
- it 'is initialized with the locked version' do
- expect(subject.version).to eq('3.0.0')
- end
- end
- end
-
- describe '#chart_above_v2?' do
- let(:elastic_stack) { create(:clusters_applications_elastic_stack, version: version) }
-
- subject { elastic_stack.chart_above_v2? }
-
- context 'on v1.9.0' do
- let(:version) { '1.9.0' }
-
- it { is_expected.to be_falsy }
- end
-
- context 'on v2.0.0' do
- let(:version) { '2.0.0' }
-
- it { is_expected.to be_truthy }
- end
- end
-
- describe '#chart_above_v3?' do
- let(:elastic_stack) { create(:clusters_applications_elastic_stack, version: version) }
-
- subject { elastic_stack.chart_above_v3? }
-
- context 'on v1.9.0' do
- let(:version) { '1.9.0' }
-
- it { is_expected.to be_falsy }
- end
-
- context 'on v3.0.0' do
- let(:version) { '3.0.0' }
-
- it { is_expected.to be_truthy }
- end
- end
-
- describe '#uninstall_command' do
- let!(:elastic_stack) { create(:clusters_applications_elastic_stack) }
-
- subject { elastic_stack.uninstall_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) }
-
- it 'is initialized with elastic stack arguments' do
- expect(subject.name).to eq('elastic-stack')
- expect(subject).to be_rbac
- expect(subject.files).to eq(elastic_stack.files)
- end
-
- it 'specifies a post delete command to remove custom resource definitions' do
- expect(subject.postdelete).to eq([
- 'kubectl delete pvc --selector app\\=elastic-stack-elasticsearch-master --namespace gitlab-managed-apps'
- ])
- end
- end
-
- it_behaves_like 'cluster-based #elasticsearch_client', :clusters_applications_elastic_stack
-end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 30591a3ff5d..65ead01a2bd 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -42,7 +42,6 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to delegate_method(:available?).to(:application_helm).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_ingress).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_knative).with_prefix }
- it { is_expected.to delegate_method(:available?).to(:integration_elastic_stack).with_prefix }
it { is_expected.to delegate_method(:available?).to(:integration_prometheus).with_prefix }
it { is_expected.to delegate_method(:external_ip).to(:application_ingress).with_prefix }
it { is_expected.to delegate_method(:external_hostname).to(:application_ingress).with_prefix }
@@ -200,22 +199,6 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
- describe '.with_available_elasticstack' do
- subject { described_class.with_available_elasticstack }
-
- let_it_be(:cluster) { create(:cluster) }
-
- context 'cluster has ElasticStack application' do
- let!(:application) { create(:clusters_applications_elastic_stack, :installed, cluster: cluster) }
-
- it { is_expected.to include(cluster) }
- end
-
- context 'cluster does not have ElasticStack application' do
- it { is_expected.not_to include(cluster) }
- end
- end
-
describe '.distinct_with_deployed_environments' do
subject { described_class.distinct_with_deployed_environments }
diff --git a/spec/models/clusters/integrations/elastic_stack_spec.rb b/spec/models/clusters/integrations/elastic_stack_spec.rb
deleted file mode 100644
index be4d59b52a2..00000000000
--- a/spec/models/clusters/integrations/elastic_stack_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Integrations::ElasticStack do
- include KubernetesHelpers
- include StubRequests
-
- describe 'associations' do
- it { is_expected.to belong_to(:cluster).class_name('Clusters::Cluster') }
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:cluster) }
- it { is_expected.not_to allow_value(nil).for(:enabled) }
- end
-
- it_behaves_like 'cluster-based #elasticsearch_client', :clusters_integrations_elastic_stack
-end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index fd89a3a2e22..92af1c3d571 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1711,25 +1711,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
end
- describe '#elastic_stack_available?' do
- let!(:cluster) { create(:cluster, :project, :provided_by_user, projects: [project]) }
- let!(:deployment) { create(:deployment, :success, environment: environment, project: project, cluster: cluster) }
-
- context 'when integration does not exist' do
- it 'returns false' do
- expect(environment.elastic_stack_available?).to be(false)
- end
- end
-
- context 'when integration is enabled' do
- let!(:integration) { create(:clusters_integrations_elastic_stack, cluster: cluster) }
-
- it 'returns true' do
- expect(environment.elastic_stack_available?).to be(true)
- end
- end
- end
-
describe '#destroy' do
it 'remove the deployment refs from gitaly' do
deployment = create(:deployment, :success, environment: environment, project: project)
diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb
index f6e398bd23c..db79185d759 100644
--- a/spec/models/project_import_state_spec.rb
+++ b/spec/models/project_import_state_spec.rb
@@ -156,7 +156,7 @@ RSpec.describe ProjectImportState, type: :model do
project.import_state.finish
end
- it 'does not qneueue housekeeping when project does not have a valid import type' do
+ it 'does not enqueue housekeeping when project does not have a valid import type' do
project = create(:project, :import_started, import_type: nil)
expect(Projects::AfterImportWorker).not_to receive(:perform_async)
@@ -164,6 +164,43 @@ RSpec.describe ProjectImportState, type: :model do
project.import_state.finish
end
end
+
+ context 'state transition: [:none, :scheduled, :started] => [:canceled]' do
+ it 'updates the import status' do
+ import_state = create(:import_state, :none)
+ expect { import_state.cancel }
+ .to change { import_state.status }
+ .from('none').to('canceled')
+ end
+
+ it 'unsets the JID' do
+ import_state = create(:import_state, :started, jid: '123')
+
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:unset)
+ .with('123')
+ .and_call_original
+
+ import_state.cancel!
+
+ expect(import_state.jid).to be_nil
+ end
+
+ it 'removes import data' do
+ import_data = ProjectImportData.new(data: { 'test' => 'some data' })
+ project = create(:project, :import_scheduled, import_data: import_data)
+
+ expect(project)
+ .to receive(:remove_import_data)
+ .and_call_original
+
+ expect do
+ project.import_state.cancel
+ project.reload
+ end.to change { project.import_data }
+ .from(import_data).to(nil)
+ end
+ end
end
describe 'clearing `jid` after finish', :clean_gitlab_redis_cache do
@@ -178,7 +215,7 @@ RSpec.describe ProjectImportState, type: :model do
end
end
- context 'with an JID' do
+ context 'with a JID' do
it 'unsets the JID' do
import_state = create(:import_state, :started, jid: '123')
diff --git a/spec/requests/api/graphql/current_user/groups_query_spec.rb b/spec/requests/api/graphql/current_user/groups_query_spec.rb
index 39f323b21a3..ef0f32bacf0 100644
--- a/spec/requests/api/graphql/current_user/groups_query_spec.rb
+++ b/spec/requests/api/graphql/current_user/groups_query_spec.rb
@@ -8,8 +8,9 @@ RSpec.describe 'Query current user groups' do
let_it_be(:user) { create(:user) }
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') }
- let_it_be(:public_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
- let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer') }
+ let_it_be(:public_developer_group) { create(:group, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
+ let_it_be(:public_maintainer_group) { create(:group, name: 'a public maintainer', path: 'a-public-maintainer') }
+ let_it_be(:public_owner_group) { create(:group, name: 'a public owner', path: 'a-public-owner') }
let(:group_arguments) { {} }
let(:current_user) { user }
@@ -29,6 +30,7 @@ RSpec.describe 'Query current user groups' do
private_maintainer_group.add_maintainer(user)
public_developer_group.add_developer(user)
public_maintainer_group.add_maintainer(user)
+ public_owner_group.add_owner(user)
end
subject { graphql_data.dig('currentUser', 'groups', 'nodes') }
@@ -52,6 +54,7 @@ RSpec.describe 'Query current user groups' do
is_expected.to match(
expected_group_hash(
public_maintainer_group,
+ public_owner_group,
private_maintainer_group,
public_developer_group,
guest_group
@@ -66,6 +69,7 @@ RSpec.describe 'Query current user groups' do
is_expected.to match(
expected_group_hash(
public_maintainer_group,
+ public_owner_group,
private_maintainer_group,
public_developer_group
)
@@ -86,6 +90,32 @@ RSpec.describe 'Query current user groups' do
end
end
+ context 'when permission_scope is TRANSFER_PROJECTS' do
+ let(:group_arguments) { { permission_scope: :TRANSFER_PROJECTS } }
+
+ specify do
+ is_expected.to match(
+ expected_group_hash(
+ public_maintainer_group,
+ public_owner_group,
+ private_maintainer_group
+ )
+ )
+ end
+
+ context 'when search is provided' do
+ let(:group_arguments) { { permission_scope: :TRANSFER_PROJECTS, search: 'owner' } }
+
+ specify do
+ is_expected.to match(
+ expected_group_hash(
+ public_owner_group
+ )
+ )
+ end
+ end
+ end
+
context 'when search is provided' do
let(:group_arguments) { { search: 'maintainer' } }
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index 7e6d80c047c..8655e5b0238 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -462,6 +462,16 @@ RSpec.describe API::ProjectImport, :aggregate_failures do
expect(json_response).to include('import_status' => 'failed',
'import_error' => 'error')
end
+
+ it 'returns the import status if canceled' do
+ project = create(:project, :import_canceled)
+ project.add_maintainer(user)
+
+ get api("/projects/#{project.id}/import", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include('import_status' => 'canceled')
+ end
end
describe 'POST /projects/import/authorize' do
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index eb907377ca8..00a67a9b2ef 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -168,29 +168,6 @@ RSpec.describe Clusters::Applications::CreateService do
subject
end
end
-
- context 'elastic stack application' do
- let(:params) do
- {
- application: 'elastic_stack'
- }
- end
-
- before do
- create(:clusters_applications_ingress, :installed, external_ip: "127.0.0.0", cluster: cluster)
- expect_any_instance_of(Clusters::Applications::ElasticStack)
- .to receive(:make_scheduled!)
- .and_call_original
- end
-
- it 'creates the application' do
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, :application_elastic_stack)
- end
- end
end
context 'invalid application' do
diff --git a/spec/services/clusters/integrations/create_service_spec.rb b/spec/services/clusters/integrations/create_service_spec.rb
index 6dac97ebf8f..016511a3c01 100644
--- a/spec/services/clusters/integrations/create_service_spec.rb
+++ b/spec/services/clusters/integrations/create_service_spec.rb
@@ -61,7 +61,6 @@ RSpec.describe Clusters::Integrations::CreateService, '#execute' do
end
it_behaves_like 'a cluster integration', 'prometheus'
- it_behaves_like 'a cluster integration', 'elastic_stack'
context 'when application_type is invalid' do
let(:params) do
diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
index 3cd82b8bf4d..5a32c1b40bb 100644
--- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures do
end
let_it_be(:project) { create(:project, :import_started) }
+ let_it_be(:project2) { create(:project, :import_canceled) }
let(:importer_class) { double(:importer_class, name: 'klass_name') }
let(:importer_instance) { double(:importer_instance) }
@@ -110,6 +111,27 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures do
})
end
+ it 'logs info if the import state is canceled' do
+ expect(project2.import_state.status).to eq('canceled')
+
+ expect(importer_class).not_to receive(:new)
+
+ expect(importer_instance).not_to receive(:execute)
+
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(
+ {
+ github_identifiers: nil,
+ message: 'project import canceled',
+ project_id: project2.id,
+ importer: 'klass_name'
+ }
+ )
+
+ worker.import(project2, client, { 'number' => 11, 'github_id' => 2 } )
+ end
+
it 'logs error when the import fails' do
expect(importer_class)
.to receive(:new)
diff --git a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
index 1e088929f66..0ac1733781a 100644
--- a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::GithubImport::StageMethods do
let_it_be(:project) { create(:project, :import_started, import_url: 'https://t0ken@github.com/repo/repo.git') }
+ let_it_be(:project2) { create(:project, :import_canceled) }
let(:worker) do
Class.new do
@@ -22,6 +23,37 @@ RSpec.describe Gitlab::GithubImport::StageMethods do
worker.perform(-1)
end
+ it 'returns if the import state is canceled' do
+ allow(worker)
+ .to receive(:find_project)
+ .with(project2.id)
+ .and_return(project2)
+
+ expect(worker).not_to receive(:try_import)
+
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(
+ {
+ message: 'starting stage',
+ project_id: project2.id,
+ import_stage: 'DummyStage'
+ }
+ )
+
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(
+ {
+ message: 'project import canceled',
+ project_id: project2.id,
+ import_stage: 'DummyStage'
+ }
+ )
+
+ worker.perform(project2.id)
+ end
+
it 'imports the data when the project exists' do
allow(worker)
.to receive(:find_project)
diff --git a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
index af15f465107..15bc55c1526 100644
--- a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe Gitlab::GithubImport::ImportDiffNoteWorker do
describe '#import' do
it 'imports a diff note' do
- project = double(:project, full_path: 'foo/bar', id: 1, import_state: nil)
+ import_state = create(:import_state, :started)
+ project = double(:project, full_path: 'foo/bar', id: 1, import_state: import_state)
client = double(:client)
importer = double(:importer)
hash = {
diff --git a/spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb
index 6af450151e3..03a6503fb84 100644
--- a/spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb
@@ -6,8 +6,10 @@ RSpec.describe Gitlab::GithubImport::ImportIssueEventWorker do
subject(:worker) { described_class.new }
describe '#import' do
+ let(:import_state) { create(:import_state, :started) }
+
let(:project) do
- instance_double('Project', full_path: 'foo/bar', id: 1, import_state: nil)
+ instance_double('Project', full_path: 'foo/bar', id: 1, import_state: import_state)
end
let(:client) { instance_double('Gitlab::GithubImport::Client') }
diff --git a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
index 29f21c1d184..c2a7639fde4 100644
--- a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe Gitlab::GithubImport::ImportIssueWorker do
describe '#import' do
it 'imports an issue' do
- project = double(:project, full_path: 'foo/bar', id: 1, import_state: nil)
+ import_state = create(:import_state, :started)
+ project = double(:project, full_path: 'foo/bar', id: 1, import_state: import_state)
client = double(:client)
importer = double(:importer)
hash = {
diff --git a/spec/workers/gitlab/github_import/import_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
index f4598340938..16ca5658f77 100644
--- a/spec/workers/gitlab/github_import/import_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe Gitlab::GithubImport::ImportNoteWorker do
describe '#import' do
it 'imports a note' do
- project = double(:project, full_path: 'foo/bar', id: 1, import_state: nil)
+ import_state = create(:import_state, :started)
+ project = double(:project, full_path: 'foo/bar', id: 1, import_state: import_state)
client = double(:client)
importer = double(:importer)
hash = {
diff --git a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
index faed2f8f340..59f45b437c4 100644
--- a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe Gitlab::GithubImport::ImportPullRequestWorker do
describe '#import' do
it 'imports a pull request' do
- project = double(:project, full_path: 'foo/bar', id: 1, import_state: nil)
+ import_state = create(:import_state, :started)
+ project = double(:project, full_path: 'foo/bar', id: 1, import_state: import_state)
client = double(:client)
importer = double(:importer)
hash = {