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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql5
-rw-r--r--app/controllers/concerns/harbor/access.rb27
-rw-r--r--app/controllers/concerns/harbor/artifact.rb41
-rw-r--r--app/controllers/concerns/harbor/repository.rb51
-rw-r--r--app/controllers/concerns/harbor/tag.rb41
-rw-r--r--app/controllers/groups/harbor/application_controller.rb16
-rw-r--r--app/controllers/groups/harbor/artifacts_controller.rb15
-rw-r--r--app/controllers/groups/harbor/repositories_controller.rb17
-rw-r--r--app/controllers/groups/harbor/tags_controller.rb15
-rw-r--r--app/controllers/projects/harbor/application_controller.rb12
-rw-r--r--app/controllers/projects/harbor/artifacts_controller.rb15
-rw-r--r--app/controllers/projects/harbor/repositories_controller.rb8
-rw-r--r--app/controllers/projects/harbor/tags_controller.rb15
-rw-r--r--app/models/ci/group_variable.rb4
-rw-r--r--app/models/ci/variable.rb4
-rw-r--r--app/models/concerns/integrations/has_issue_tracker_fields.rb18
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/integrations/campfire.rb63
-rw-r--r--app/models/integrations/datadog.rb157
-rw-r--r--app/models/integrations/harbor.rb2
-rw-r--r--app/models/integrations/irker.rb78
-rw-r--r--app/models/integrations/packagist.rb51
-rw-r--r--app/models/integrations/pipelines_email.rb34
-rw-r--r--app/models/integrations/pushover.rb141
-rw-r--r--app/models/integrations/shimo.rb16
-rw-r--r--app/models/integrations/youtrack.rb5
-rw-r--r--app/models/integrations/zentao.rb56
-rw-r--r--app/policies/group_policy.rb1
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/serializers/integrations/harbor_serializers/artifact_entity.rb29
-rw-r--r--app/serializers/integrations/harbor_serializers/artifact_serializer.rb11
-rw-r--r--app/serializers/integrations/harbor_serializers/repository_entity.rb57
-rw-r--r--app/serializers/integrations/harbor_serializers/repository_serializer.rb11
-rw-r--r--app/serializers/integrations/harbor_serializers/tag_entity.rb41
-rw-r--r--app/serializers/integrations/harbor_serializers/tag_serializer.rb11
-rw-r--r--app/views/groups/harbor/repositories/index.html.haml2
-rw-r--r--app/views/groups/merge_requests.html.haml2
-rw-r--r--app/views/projects/harbor/repositories/index.html.haml2
-rw-r--r--config/routes/group.rb9
-rw-r--r--config/routes/project.rb11
-rw-r--r--db/post_migrate/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences.rb20
-rw-r--r--db/post_migrate/20220630085003_drop_project_successfull_pages_deploy_index_from_ci_builds.rb21
-rw-r--r--db/schema_migrations/202206060829101
-rw-r--r--db/schema_migrations/202206300850031
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/geo/replication/object_storage.md3
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/user/infrastructure/iac/terraform_state.md10
-rw-r--r--lib/api/helpers/integrations_helpers.rb6
-rw-r--r--lib/gitlab/harbor/client.rb38
-rw-r--r--lib/gitlab/harbor/query.rb126
-rw-r--r--lib/sidebars/groups/menus/packages_registries_menu.rb2
-rw-r--r--lib/sidebars/projects/menus/packages_registries_menu.rb4
-rw-r--r--locale/gitlab.pot9
-rw-r--r--spec/controllers/concerns/harbor/artifact_spec.rb10
-rw-r--r--spec/controllers/concerns/harbor/repository_spec.rb10
-rw-r--r--spec/controllers/concerns/harbor/tag_spec.rb10
-rw-r--r--spec/factories/integrations.rb2
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/lib/gitlab/harbor/client_spec.rb269
-rw-r--r--spec/lib/gitlab/harbor/query_spec.rb375
-rw-r--r--spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb22
-rw-r--r--spec/models/ci/group_variable_spec.rb6
-rw-r--r--spec/models/ci/variable_spec.rb6
-rw-r--r--spec/models/group_spec.rb1
-rw-r--r--spec/models/integrations/datadog_spec.rb16
-rw-r--r--spec/models/integrations/harbor_spec.rb8
-rw-r--r--spec/models/integrations/youtrack_spec.rb6
-rw-r--r--spec/requests/groups/harbor/artifacts_controller_spec.rb10
-rw-r--r--spec/requests/groups/harbor/repositories_controller_spec.rb65
-rw-r--r--spec/requests/groups/harbor/tags_controller_spec.rb10
-rw-r--r--spec/requests/projects/harbor/artifacts_controller_spec.rb10
-rw-r--r--spec/requests/projects/harbor/repositories_controller_spec.rb65
-rw-r--r--spec/requests/projects/harbor/tags_controller_spec.rb10
-rw-r--r--spec/routing/group_routing_spec.rb12
-rw-r--r--spec/serializers/integrations/harbor_serializers/artifact_entity_spec.rb51
-rw-r--r--spec/serializers/integrations/harbor_serializers/artifact_serializer_spec.rb9
-rw-r--r--spec/serializers/integrations/harbor_serializers/repository_entity_spec.rb55
-rw-r--r--spec/serializers/integrations/harbor_serializers/repository_serializer_spec.rb9
-rw-r--r--spec/serializers/integrations/harbor_serializers/tag_entity_spec.rb38
-rw-r--r--spec/serializers/integrations/harbor_serializers/tag_serializer_spec.rb9
-rw-r--r--spec/support/helpers/harbor_helper.rb27
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb1
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb2
-rw-r--r--spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb162
-rw-r--r--spec/support/shared_examples/harbor/container_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb172
-rw-r--r--spec/support/shared_examples/harbor/tags_controller_shared_examples.rb155
89 files changed, 2457 insertions, 511 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index cc97bd7d197..042664f7338 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-bfcc916ab8ef38ee53755e2d71fc45d28f26c7bf
+53fd83a9c21e89bf1bfb9b7f918b9bcfa3ef776a
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
index 470de060ee3..ad861a60d15 100644
--- a/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
@@ -1,8 +1,13 @@
+#import "./work_item.fragment.graphql"
+
mutation workItemUpdateTask($input: WorkItemUpdateTaskInput!) {
workItemUpdate: workItemUpdateTask(input: $input) {
workItem {
id
descriptionHtml
}
+ task {
+ ...WorkItem
+ }
}
}
diff --git a/app/controllers/concerns/harbor/access.rb b/app/controllers/concerns/harbor/access.rb
new file mode 100644
index 00000000000..70de72f15fc
--- /dev/null
+++ b/app/controllers/concerns/harbor/access.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Harbor
+ module Access
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :harbor_registry_enabled!
+ before_action :authorize_read_harbor_registry!
+ before_action do
+ push_frontend_feature_flag(:harbor_registry_integration)
+ end
+
+ feature_category :integrations
+ end
+
+ private
+
+ def harbor_registry_enabled!
+ render_404 unless Feature.enabled?(:harbor_registry_integration)
+ end
+
+ def authorize_read_harbor_registry!
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/controllers/concerns/harbor/artifact.rb b/app/controllers/concerns/harbor/artifact.rb
new file mode 100644
index 00000000000..c9d7d26fbb9
--- /dev/null
+++ b/app/controllers/concerns/harbor/artifact.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Harbor
+ module Artifact
+ def index
+ respond_to do |format|
+ format.json do
+ artifacts
+ end
+ end
+ end
+
+ private
+
+ def query_params
+ params.permit(:repository_id, :search, :sort, :page, :limit)
+ end
+
+ def query
+ Gitlab::Harbor::Query.new(container.harbor_integration, query_params)
+ end
+
+ def artifacts
+ unless query.valid?
+ return render(
+ json: { message: 'Invalid parameters', errors: query.errors },
+ status: :unprocessable_entity
+ )
+ end
+
+ artifacts_json = ::Integrations::HarborSerializers::ArtifactSerializer.new
+ .with_pagination(request, response)
+ .represent(query.artifacts)
+ render json: artifacts_json
+ end
+
+ def container
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/controllers/concerns/harbor/repository.rb b/app/controllers/concerns/harbor/repository.rb
new file mode 100644
index 00000000000..0e541e2172e
--- /dev/null
+++ b/app/controllers/concerns/harbor/repository.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Harbor
+ module Repository
+ def index
+ respond_to do |format|
+ format.html
+ format.json do
+ repositories
+ end
+ end
+ end
+
+ # The show action renders index to allow frontend routing to work on page refresh
+ def show
+ render :index
+ end
+
+ private
+
+ def query_params
+ params.permit(:search, :sort, :page, :limit)
+ end
+
+ def query
+ Gitlab::Harbor::Query.new(container.harbor_integration, query_params)
+ end
+
+ def repositories
+ unless query.valid?
+ return render(
+ json: { message: 'Invalid parameters', errors: query.errors },
+ status: :unprocessable_entity
+ )
+ end
+
+ repositories_json = ::Integrations::HarborSerializers::RepositorySerializer.new
+ .with_pagination(request, response)
+ .represent(
+ query.repositories,
+ url: container.harbor_integration.url,
+ project_name: container.harbor_integration.project_name
+ )
+ render json: repositories_json
+ end
+
+ def container
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/controllers/concerns/harbor/tag.rb b/app/controllers/concerns/harbor/tag.rb
new file mode 100644
index 00000000000..e0c00d1155a
--- /dev/null
+++ b/app/controllers/concerns/harbor/tag.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Harbor
+ module Tag
+ def index
+ respond_to do |format|
+ format.json do
+ tags
+ end
+ end
+ end
+
+ private
+
+ def query_params
+ params.permit(:repository_id, :artifact_id, :sort, :page, :limit)
+ end
+
+ def query
+ Gitlab::Harbor::Query.new(container.harbor_integration, query_params)
+ end
+
+ def tags
+ unless query.valid?
+ return render(
+ json: { message: 'Invalid parameters', errors: query.errors },
+ status: :unprocessable_entity
+ )
+ end
+
+ tags_json = ::Integrations::HarborSerializers::TagSerializer.new
+ .with_pagination(request, response)
+ .represent(query.tags)
+ render json: tags_json
+ end
+
+ def container
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/controllers/groups/harbor/application_controller.rb b/app/controllers/groups/harbor/application_controller.rb
new file mode 100644
index 00000000000..cff767c8efd
--- /dev/null
+++ b/app/controllers/groups/harbor/application_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Groups
+ module Harbor
+ class ApplicationController < Groups::ApplicationController
+ layout 'group'
+ include ::Harbor::Access
+
+ private
+
+ def authorize_read_harbor_registry!
+ render_404 unless can?(current_user, :read_harbor_registry, @group)
+ end
+ end
+ end
+end
diff --git a/app/controllers/groups/harbor/artifacts_controller.rb b/app/controllers/groups/harbor/artifacts_controller.rb
new file mode 100644
index 00000000000..b7570b44a2c
--- /dev/null
+++ b/app/controllers/groups/harbor/artifacts_controller.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Groups
+ module Harbor
+ class ArtifactsController < ::Groups::Harbor::ApplicationController
+ include ::Harbor::Artifact
+
+ private
+
+ def container
+ @group
+ end
+ end
+ end
+end
diff --git a/app/controllers/groups/harbor/repositories_controller.rb b/app/controllers/groups/harbor/repositories_controller.rb
index 364607f9b20..1ad38bd7103 100644
--- a/app/controllers/groups/harbor/repositories_controller.rb
+++ b/app/controllers/groups/harbor/repositories_controller.rb
@@ -2,22 +2,13 @@
module Groups
module Harbor
- class RepositoriesController < Groups::ApplicationController
- feature_category :integrations
-
- before_action :harbor_registry_enabled!
- before_action do
- push_frontend_feature_flag(:harbor_registry_integration)
- end
-
- def show
- render :index
- end
+ class RepositoriesController < ::Groups::Harbor::ApplicationController
+ include ::Harbor::Repository
private
- def harbor_registry_enabled!
- render_404 unless Feature.enabled?(:harbor_registry_integration)
+ def container
+ @group
end
end
end
diff --git a/app/controllers/groups/harbor/tags_controller.rb b/app/controllers/groups/harbor/tags_controller.rb
new file mode 100644
index 00000000000..da43cb3f64c
--- /dev/null
+++ b/app/controllers/groups/harbor/tags_controller.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Groups
+ module Harbor
+ class TagsController < ::Groups::Harbor::ApplicationController
+ include ::Harbor::Tag
+
+ private
+
+ def container
+ @group
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/harbor/application_controller.rb b/app/controllers/projects/harbor/application_controller.rb
index e6e694783fa..9271ec560dc 100644
--- a/app/controllers/projects/harbor/application_controller.rb
+++ b/app/controllers/projects/harbor/application_controller.rb
@@ -4,18 +4,12 @@ module Projects
module Harbor
class ApplicationController < Projects::ApplicationController
layout 'project'
-
- before_action :harbor_registry_enabled!
- before_action do
- push_frontend_feature_flag(:harbor_registry_integration)
- end
-
- feature_category :integrations
+ include ::Harbor::Access
private
- def harbor_registry_enabled!
- render_404 unless Feature.enabled?(:harbor_registry_integration)
+ def authorize_read_harbor_registry!
+ render_404 unless can?(current_user, :read_harbor_registry, @project)
end
end
end
diff --git a/app/controllers/projects/harbor/artifacts_controller.rb b/app/controllers/projects/harbor/artifacts_controller.rb
new file mode 100644
index 00000000000..ce36f181b42
--- /dev/null
+++ b/app/controllers/projects/harbor/artifacts_controller.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Projects
+ module Harbor
+ class ArtifactsController < ::Projects::Harbor::ApplicationController
+ include ::Harbor::Artifact
+
+ private
+
+ def container
+ @project
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/harbor/repositories_controller.rb b/app/controllers/projects/harbor/repositories_controller.rb
index dd3e3dc1978..4db13331bf0 100644
--- a/app/controllers/projects/harbor/repositories_controller.rb
+++ b/app/controllers/projects/harbor/repositories_controller.rb
@@ -3,8 +3,12 @@
module Projects
module Harbor
class RepositoriesController < ::Projects::Harbor::ApplicationController
- def show
- render :index
+ include ::Harbor::Repository
+
+ private
+
+ def container
+ @project
end
end
end
diff --git a/app/controllers/projects/harbor/tags_controller.rb b/app/controllers/projects/harbor/tags_controller.rb
new file mode 100644
index 00000000000..f49c5ac7768
--- /dev/null
+++ b/app/controllers/projects/harbor/tags_controller.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Projects
+ module Harbor
+ class TagsController < ::Projects::Harbor::ApplicationController
+ include ::Harbor::Tag
+
+ private
+
+ def container
+ @project
+ end
+ end
+ end
+end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 0af5533613f..e11edbda6dc 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -19,5 +19,9 @@ module Ci
scope :unprotected, -> { where(protected: false) }
scope :by_environment_scope, -> (environment_scope) { where(environment_scope: environment_scope) }
scope :for_groups, ->(group_ids) { where(group_id: group_ids) }
+
+ def audit_details
+ key
+ end
end
end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 1e91f248fc4..c80c2ebe69a 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -18,5 +18,9 @@ module Ci
scope :unprotected, -> { where(protected: false) }
scope :by_environment_scope, -> (environment_scope) { where(environment_scope: environment_scope) }
+
+ def audit_details
+ key
+ end
end
end
diff --git a/app/models/concerns/integrations/has_issue_tracker_fields.rb b/app/models/concerns/integrations/has_issue_tracker_fields.rb
index b1def38d019..57f8e21c5a6 100644
--- a/app/models/concerns/integrations/has_issue_tracker_fields.rb
+++ b/app/models/concerns/integrations/has_issue_tracker_fields.rb
@@ -5,26 +5,32 @@ module Integrations
extend ActiveSupport::Concern
included do
+ self.field_storage = :data_fields
+
field :project_url,
required: true,
- storage: :data_fields,
title: -> { _('Project URL') },
- help: -> { s_('IssueTracker|The URL to the project in the external issue tracker.') }
+ help: -> do
+ s_('IssueTracker|The URL to the project in the external issue tracker.')
+ end
field :issues_url,
required: true,
- storage: :data_fields,
title: -> { s_('IssueTracker|Issue URL') },
help: -> do
- format s_('IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}.'),
+ ERB::Util.html_escape(
+ s_('IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}.')
+ ) % {
colon_id: '<code>:id</code>'.html_safe
+ }
end
field :new_issue_url,
required: true,
- storage: :data_fields,
title: -> { s_('IssueTracker|New issue URL') },
- help: -> { s_('IssueTracker|The URL to create an issue in the external issue tracker.') }
+ help: -> do
+ s_('IssueTracker|The URL to create an issue in the external issue tracker.')
+ end
end
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 5919b1e71bf..6d8f8bd7613 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -112,6 +112,8 @@ class Group < Namespace
has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob'
has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest'
+ has_one :harbor_integration, class_name: 'Integrations::Harbor'
+
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb
index 7889cd8f9a9..bf1358ac0f6 100644
--- a/app/models/integrations/campfire.rb
+++ b/app/models/integrations/campfire.rb
@@ -2,9 +2,34 @@
module Integrations
class Campfire < Integration
- prop_accessor :token, :subdomain, :room
validates :token, presence: true, if: :activated?
+ field :token,
+ type: 'password',
+ title: -> { _('Campfire token') },
+ help: -> { s_('CampfireService|API authentication token from Campfire.') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new token') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
+ placeholder: '',
+ required: true
+
+ field :subdomain,
+ title: -> { _('Campfire subdomain (optional)') },
+ placeholder: '',
+ help: -> do
+ ERB::Util.html_escape(
+ s_('CampfireService|The %{code_open}.campfirenow.com%{code_close} subdomain.')
+ ) % {
+ code_open: '<code>'.html_safe,
+ code_close: '</code>'.html_safe
+ }
+ end
+
+ field :room,
+ title: -> { _('Campfire room ID (optional)') },
+ placeholder: '123456',
+ help: -> { s_('CampfireService|From the end of the room URL.') }
+
def title
'Campfire'
end
@@ -15,42 +40,18 @@ module Integrations
def help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'campfire'), target: '_blank', rel: 'noopener noreferrer'
- s_('CampfireService|Send notifications about push events to Campfire chat rooms. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+
+ ERB::Util.html_escape(
+ s_('CampfireService|Send notifications about push events to Campfire chat rooms. %{docs_link}')
+ ) % {
+ docs_link: docs_link.html_safe
+ }
end
def self.to_param
'campfire'
end
- def fields
- [
- {
- type: 'password',
- name: 'token',
- title: _('Campfire token'),
- help: s_('CampfireService|API authentication token from Campfire.'),
- non_empty_password_title: s_('ProjectService|Enter new token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
- placeholder: '',
- required: true
- },
- {
- type: 'text',
- name: 'subdomain',
- title: _('Campfire subdomain (optional)'),
- placeholder: '',
- help: s_('CampfireService|The %{code_open}.campfirenow.com%{code_close} subdomain.') % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- },
- {
- type: 'text',
- name: 'room',
- title: _('Campfire room ID (optional)'),
- placeholder: '123456',
- help: s_('CampfireService|From the end of the room URL.')
- }
- ]
- end
-
def self.supported_events
%w(push)
end
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index bb0fb6b9079..97e586c0662 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -15,7 +15,75 @@ module Integrations
TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x.freeze
- prop_accessor :datadog_site, :api_url, :api_key, :datadog_service, :datadog_env, :datadog_tags
+ field :datadog_site,
+ placeholder: DEFAULT_DOMAIN,
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|The Datadog site to send data to. To send data to the EU site, use %{codeOpen}datadoghq.eu%{codeClose}.')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe
+ }
+ end
+
+ field :api_url,
+ title: -> { s_('DatadogIntegration|API URL') },
+ help: -> { s_('DatadogIntegration|(Advanced) The full URL for your Datadog site.') }
+
+ field :api_key,
+ type: 'password',
+ title: -> { _('API key') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new API key') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current API key') },
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|%{linkOpen}API key%{linkClose} used for authentication with Datadog.')
+ ) % {
+ linkOpen: %Q{<a href="#{URL_API_KEYS_DOCS}" target="_blank" rel="noopener noreferrer">}.html_safe,
+ linkClose: '</a>'.html_safe
+ }
+ end,
+ required: true
+
+ field :archive_trace_events,
+ type: 'checkbox',
+ title: -> { s_('Logs') },
+ checkbox_label: -> { s_('Enable logs collection') },
+ help: -> { s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.') }
+
+ field :datadog_service,
+ title: -> { s_('DatadogIntegration|Service') },
+ placeholder: 'gitlab-ci',
+ help: -> { s_('DatadogIntegration|Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments.') }
+
+ field :datadog_env,
+ title: -> { s_('DatadogIntegration|Environment') },
+ placeholder: 'ci',
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|For self-managed deployments, set the %{codeOpen}env%{codeClose} tag for all the data sent to Datadog. %{linkOpen}How do I use tags?%{linkClose}')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe,
+ linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
+ linkClose: '</a>'.html_safe
+ }
+ end
+
+ field :datadog_tags,
+ type: 'textarea',
+ title: -> { s_('DatadogIntegration|Tags') },
+ placeholder: "tag:value\nanother_tag:value",
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|Custom tags in Datadog. Enter one tag per line in the %{codeOpen}key:value%{codeClose} format. %{linkOpen}How do I use tags?%{linkClose}')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe,
+ linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
+ linkClose: '</a>'.html_safe
+ }
+ end
before_validation :strip_properties
@@ -77,92 +145,11 @@ module Integrations
end
def fields
- f = [
- {
- type: 'text',
- name: 'datadog_site',
- placeholder: DEFAULT_DOMAIN,
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|The Datadog site to send data to. To send data to the EU site, use %{codeOpen}datadoghq.eu%{codeClose}.')
- ) % {
- codeOpen: '<code>'.html_safe,
- codeClose: '</code>'.html_safe
- },
- required: false
- },
- {
- type: 'text',
- name: 'api_url',
- title: s_('DatadogIntegration|API URL'),
- help: s_('DatadogIntegration|(Advanced) The full URL for your Datadog site.'),
- required: false
- },
- {
- type: 'password',
- name: 'api_key',
- title: _('API key'),
- non_empty_password_title: s_('ProjectService|Enter new API key'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'),
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|%{linkOpen}API key%{linkClose} used for authentication with Datadog.')
- ) % {
- linkOpen: %Q{<a href="#{URL_API_KEYS_DOCS}" target="_blank" rel="noopener noreferrer">}.html_safe,
- linkClose: '</a>'.html_safe
- },
- required: true
- }
- ]
-
if Feature.enabled?(:datadog_integration_logs_collection, parent)
- f.append({
- type: 'checkbox',
- name: 'archive_trace_events',
- title: s_('Logs'),
- checkbox_label: s_('Enable logs collection'),
- help: s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.'),
- required: false
- })
+ super
+ else
+ super.reject { _1.name == 'archive_trace_events' }
end
-
- f += [
- {
- type: 'text',
- name: 'datadog_service',
- title: s_('DatadogIntegration|Service'),
- placeholder: 'gitlab-ci',
- help: s_('DatadogIntegration|Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments.')
- },
- {
- type: 'text',
- name: 'datadog_env',
- title: s_('DatadogIntegration|Environment'),
- placeholder: 'ci',
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|For self-managed deployments, set the %{codeOpen}env%{codeClose} tag for all the data sent to Datadog. %{linkOpen}How do I use tags?%{linkClose}')
- ) % {
- codeOpen: '<code>'.html_safe,
- codeClose: '</code>'.html_safe,
- linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
- linkClose: '</a>'.html_safe
- }
- },
- {
- type: 'textarea',
- name: 'datadog_tags',
- title: s_('DatadogIntegration|Tags'),
- placeholder: "tag:value\nanother_tag:value",
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|Custom tags in Datadog. Enter one tag per line in the %{codeOpen}key:value%{codeClose} format. %{linkOpen}How do I use tags?%{linkClose}')
- ) % {
- codeOpen: '<code>'.html_safe,
- codeClose: '</code>'.html_safe,
- linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
- linkClose: '</a>'.html_safe
- }
- }
- ]
-
- f
end
override :hook_url
diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb
index 44813795fc0..82981493822 100644
--- a/app/models/integrations/harbor.rb
+++ b/app/models/integrations/harbor.rb
@@ -4,7 +4,7 @@ module Integrations
class Harbor < Integration
prop_accessor :url, :project_name, :username, :password
- validates :url, public_url: true, presence: true, if: :activated?
+ validates :url, public_url: true, presence: true, addressable_url: { allow_localhost: false, allow_local_network: false }, if: :activated?
validates :project_name, presence: true, if: :activated?
validates :username, presence: true, if: :activated?
validates :password, format: { with: ::Ci::Maskable::REGEX }, if: :activated?
diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb
index 0ec750a4b77..3f3e321f45e 100644
--- a/app/models/integrations/irker.rb
+++ b/app/models/integrations/irker.rb
@@ -4,13 +4,55 @@ require 'uri'
module Integrations
class Irker < Integration
- prop_accessor :server_host, :server_port, :default_irc_uri
- prop_accessor :recipients, :channels
- boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :validate_recipients?
-
before_validation :get_channels
+ field :server_host,
+ placeholder: 'localhost',
+ title: -> { s_('IrkerService|Server host (optional)') },
+ help: -> { s_('IrkerService|irker daemon hostname (defaults to localhost).') }
+
+ field :server_port,
+ placeholder: 6659,
+ title: -> { s_('IrkerService|Server port (optional)') },
+ help: -> { s_('IrkerService|irker daemon port (defaults to 6659).') }
+
+ field :default_irc_uri,
+ title: -> { s_('IrkerService|Default IRC URI (optional)') },
+ help: -> { s_('IrkerService|URI to add before each recipient.') },
+ placeholder: 'irc://irc.network.net:6697/'
+
+ field :recipients,
+ type: 'textarea',
+ title: -> { s_('IrkerService|Recipients') },
+ placeholder: 'irc[s]://irc.network.net[:port]/#channel',
+ required: true,
+ help: -> do
+ recipients_docs_link = ActionController::Base.helpers.link_to(
+ s_('IrkerService|How to enter channels or users?'),
+ Rails.application.routes.url_helpers.help_page_url(
+ 'user/project/integrations/irker',
+ anchor: 'enter-irker-recipients'
+ ),
+ target: '_blank', rel: 'noopener noreferrer'
+ )
+
+ ERB::Util.html_escape(
+ s_('IrkerService|Channels and users separated by whitespaces. %{recipients_docs_link}')
+ ) % {
+ recipients_docs_link: recipients_docs_link.html_safe
+ }
+ end
+
+ field :colorize_messages,
+ type: 'checkbox',
+ title: -> { _('Colorize messages') }
+
+ # NOTE: This field is only used internally to store the parsed
+ # channels from the `recipients` field, it should not be exposed
+ # in the UI or API.
+ prop_accessor :channels
+
def title
s_('IrkerService|irker (IRC gateway)')
end
@@ -43,34 +85,6 @@ module Integrations
}
end
- def fields
- recipients_docs_link = ActionController::Base.helpers.link_to(
- s_('IrkerService|How to enter channels or users?'),
- Rails.application.routes.url_helpers.help_page_url(
- 'user/project/integrations/irker',
- anchor: 'enter-irker-recipients'
- ),
- target: '_blank', rel: 'noopener noreferrer'
- )
-
- [
- { type: 'text', name: 'server_host', placeholder: 'localhost', title: s_('IrkerService|Server host (optional)'),
- help: s_('IrkerService|irker daemon hostname (defaults to localhost).') },
- { type: 'text', name: 'server_port', placeholder: 6659, title: s_('IrkerService|Server port (optional)'),
- help: s_('IrkerService|irker daemon port (defaults to 6659).') },
- { type: 'text', name: 'default_irc_uri', title: s_('IrkerService|Default IRC URI (optional)'),
- help: s_('IrkerService|URI to add before each recipient.'),
- placeholder: 'irc://irc.network.net:6697/' },
- { type: 'textarea', name: 'recipients', title: s_('IrkerService|Recipients'),
- placeholder: 'irc[s]://irc.network.net[:port]/#channel', required: true,
- help: format(
- s_('IrkerService|Channels and users separated by whitespaces. %{recipients_docs_link}').html_safe,
- recipients_docs_link: recipients_docs_link.html_safe
- ) },
- { type: 'checkbox', name: 'colorize_messages', title: _('Colorize messages') }
- ]
- end
-
def help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index 758c9e4761b..05ee919892d 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -5,7 +5,25 @@ module Integrations
include HasWebHook
extend Gitlab::Utils::Override
- prop_accessor :username, :token, :server
+ field :username,
+ title: -> { _('Username') },
+ help: -> { s_('Enter your Packagist username.') },
+ placeholder: '',
+ required: true
+
+ field :token,
+ type: 'password',
+ title: -> { _('Token') },
+ help: -> { s_('Enter your Packagist token.') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new token') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
+ placeholder: '',
+ required: true
+
+ field :server,
+ title: -> { _('Server (optional)') },
+ help: -> { s_('Enter your Packagist server. Defaults to https://packagist.org.') },
+ placeholder: 'https://packagist.org'
validates :username, presence: true, if: :activated?
validates :token, presence: true, if: :activated?
@@ -22,37 +40,6 @@ module Integrations
'packagist'
end
- def fields
- [
- {
- type: 'text',
- name: 'username',
- title: _('Username'),
- help: s_('Enter your Packagist username.'),
- placeholder: '',
- required: true
- },
- {
- type: 'password',
- name: 'token',
- title: _('Token'),
- help: s_('Enter your Packagist token.'),
- non_empty_password_title: s_('ProjectService|Enter new token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
- placeholder: '',
- required: true
- },
- {
- type: 'text',
- name: 'server',
- title: _('Server (optional)'),
- help: s_('Enter your Packagist server. Defaults to https://packagist.org.'),
- placeholder: 'https://packagist.org',
- required: false
- }
- ]
- end
-
def self.supported_events
%w(push merge_request tag_push)
end
diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb
index c08d1fe7f4a..77cbba25f2c 100644
--- a/app/models/integrations/pipelines_email.rb
+++ b/app/models/integrations/pipelines_email.rb
@@ -6,11 +6,26 @@ module Integrations
RECIPIENTS_LIMIT = 30
- prop_accessor :recipients, :branches_to_be_notified
- boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :recipients, presence: true, if: :validate_recipients?
validate :number_of_recipients_within_limit, if: :validate_recipients?
+ field :recipients,
+ type: 'textarea',
+ help: -> { _('Comma-separated list of email addresses.') },
+ required: true
+
+ field :notify_only_broken_pipelines,
+ type: 'checkbox'
+
+ field :notify_only_default_branch,
+ type: 'checkbox',
+ api_only: true
+
+ field :branches_to_be_notified,
+ type: 'select',
+ title: -> { s_('Integrations|Branches for which notifications are to be sent') },
+ choices: branch_choices
+
def initialize_properties
super
@@ -65,21 +80,6 @@ module Integrations
project&.ci_pipelines&.any?
end
- def fields
- [
- { type: 'textarea',
- name: 'recipients',
- help: _('Comma-separated list of email addresses.'),
- required: true },
- { type: 'checkbox',
- name: 'notify_only_broken_pipelines' },
- { type: 'select',
- name: 'branches_to_be_notified',
- title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: self.class.branch_choices }
- ]
- end
-
def test(data)
result = execute(data, force: true)
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index 7fd5efa8765..791e27c5db7 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -4,9 +4,73 @@ module Integrations
class Pushover < Integration
BASE_URI = 'https://api.pushover.net/1'
- prop_accessor :api_key, :user_key, :device, :priority, :sound
validates :api_key, :user_key, :priority, presence: true, if: :activated?
+ field :api_key,
+ type: 'password',
+ title: -> { _('API key') },
+ help: -> { s_('PushoverService|Enter your application key.') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new API key') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current API key.') },
+ placeholder: '',
+ required: true
+
+ field :user_key,
+ type: 'password',
+ title: -> { _('User key') },
+ help: -> { s_('PushoverService|Enter your user key.') },
+ non_empty_password_title: -> { s_('PushoverService|Enter new user key') },
+ non_empty_password_help: -> { s_('PushoverService|Leave blank to use your current user key.') },
+ placeholder: '',
+ required: true
+
+ field :device,
+ title: -> { _('Devices (optional)') },
+ help: -> { s_('PushoverService|Leave blank for all active devices.') },
+ placeholder: ''
+
+ field :priority,
+ type: 'select',
+ required: true,
+ choices: -> do
+ [
+ [s_('PushoverService|Lowest priority'), -2],
+ [s_('PushoverService|Low priority'), -1],
+ [s_('PushoverService|Normal priority'), 0],
+ [s_('PushoverService|High priority'), 1]
+ ]
+ end
+
+ field :sound,
+ type: 'select',
+ choices: -> do
+ [
+ ['Device default sound', nil],
+ ['Pushover (default)', 'pushover'],
+ %w(Bike bike),
+ %w(Bugle bugle),
+ ['Cash Register', 'cashregister'],
+ %w(Classical classical),
+ %w(Cosmic cosmic),
+ %w(Falling falling),
+ %w(Gamelan gamelan),
+ %w(Incoming incoming),
+ %w(Intermission intermission),
+ %w(Magic magic),
+ %w(Mechanical mechanical),
+ ['Piano Bar', 'pianobar'],
+ %w(Siren siren),
+ ['Space Alarm', 'spacealarm'],
+ ['Tug Boat', 'tugboat'],
+ ['Alien Alarm (long)', 'alien'],
+ ['Climb (long)', 'climb'],
+ ['Persistent (long)', 'persistent'],
+ ['Pushover Echo (long)', 'echo'],
+ ['Up Down (long)', 'updown'],
+ ['None (silent)', 'none']
+ ]
+ end
+
def title
'Pushover'
end
@@ -19,81 +83,6 @@ module Integrations
'pushover'
end
- def fields
- [
- {
- type: 'password',
- name: 'api_key',
- title: _('API key'),
- help: s_('PushoverService|Enter your application key.'),
- non_empty_password_title: s_('ProjectService|Enter new API key'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current API key.'),
- placeholder: '',
- required: true
- },
- {
- type: 'password',
- name: 'user_key',
- title: _('User key'),
- help: s_('PushoverService|Enter your user key.'),
- non_empty_password_title: s_('PushoverService|Enter new user key'),
- non_empty_password_help: s_('PushoverService|Leave blank to use your current user key.'),
- placeholder: '',
- required: true
- },
- {
- type: 'text',
- name: 'device',
- title: _('Devices (optional)'),
- help: s_('PushoverService|Leave blank for all active devices.'),
- placeholder: ''
- },
- {
- type: 'select',
- name: 'priority',
- required: true,
- choices:
- [
- [s_('PushoverService|Lowest priority'), -2],
- [s_('PushoverService|Low priority'), -1],
- [s_('PushoverService|Normal priority'), 0],
- [s_('PushoverService|High priority'), 1]
- ],
- default_choice: 0
- },
- {
- type: 'select',
- name: 'sound',
- choices:
- [
- ['Device default sound', nil],
- ['Pushover (default)', 'pushover'],
- %w(Bike bike),
- %w(Bugle bugle),
- ['Cash Register', 'cashregister'],
- %w(Classical classical),
- %w(Cosmic cosmic),
- %w(Falling falling),
- %w(Gamelan gamelan),
- %w(Incoming incoming),
- %w(Intermission intermission),
- %w(Magic magic),
- %w(Mechanical mechanical),
- ['Piano Bar', 'pianobar'],
- %w(Siren siren),
- ['Space Alarm', 'spacealarm'],
- ['Tug Boat', 'tugboat'],
- ['Alien Alarm (long)', 'alien'],
- ['Climb (long)', 'climb'],
- ['Persistent (long)', 'persistent'],
- ['Pushover Echo (long)', 'echo'],
- ['Up Down (long)', 'updown'],
- ['None (silent)', 'none']
- ]
- }
- ]
- end
-
def self.supported_events
%w(push)
end
diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb
index 350ee61ad11..8bc296e0320 100644
--- a/app/models/integrations/shimo.rb
+++ b/app/models/integrations/shimo.rb
@@ -2,9 +2,12 @@
module Integrations
class Shimo < BaseThirdPartyWiki
- prop_accessor :external_wiki_url
validates :external_wiki_url, presence: true, public_url: true, if: :activated?
+ field :external_wiki_url,
+ title: -> { s_('Shimo|Shimo Workspace URL') },
+ required: true
+
def render?
return false unless Feature.enabled?(:shimo_integration, project)
@@ -30,16 +33,5 @@ module Integrations
rescue StandardError
nil
end
-
- def fields
- [
- {
- type: 'text',
- name: 'external_wiki_url',
- title: s_('Shimo|Shimo Workspace URL'),
- required: true
- }
- ]
- end
end
end
diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb
index ab6e1da27f8..fa719f925ed 100644
--- a/app/models/integrations/youtrack.rb
+++ b/app/models/integrations/youtrack.rb
@@ -33,10 +33,7 @@ module Integrations
end
def fields
- [
- { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in YouTrack.'), required: true },
- { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the YouTrack project. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true }
- ]
+ super.select { _1.name.in?(%w[project_url issues_url]) }
end
end
end
diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb
index c33df465fde..11db469f7ee 100644
--- a/app/models/integrations/zentao.rb
+++ b/app/models/integrations/zentao.rb
@@ -4,7 +4,28 @@ module Integrations
class Zentao < Integration
include Gitlab::Routing
- data_field :url, :api_url, :api_token, :zentao_product_xid
+ self.field_storage = :data_fields
+
+ field :url,
+ title: -> { s_('ZentaoIntegration|ZenTao Web URL') },
+ placeholder: 'https://www.zentao.net',
+ help: -> { s_('ZentaoIntegration|Base URL of the ZenTao instance.') },
+ required: true
+
+ field :api_url,
+ title: -> { s_('ZentaoIntegration|ZenTao API URL (optional)') },
+ help: -> { s_('ZentaoIntegration|If different from Web URL.') }
+
+ field :api_token,
+ type: 'password',
+ title: -> { s_('ZentaoIntegration|ZenTao API token') },
+ non_empty_password_title: -> { s_('ZentaoIntegration|Enter new ZenTao API token') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
+ required: true
+
+ field :zentao_product_xid,
+ title: -> { s_('ZentaoIntegration|ZenTao Product ID') },
+ required: true
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
@@ -47,39 +68,6 @@ module Integrations
%w()
end
- def fields
- [
- {
- type: 'text',
- name: 'url',
- title: s_('ZentaoIntegration|ZenTao Web URL'),
- placeholder: 'https://www.zentao.net',
- help: s_('ZentaoIntegration|Base URL of the ZenTao instance.'),
- required: true
- },
- {
- type: 'text',
- name: 'api_url',
- title: s_('ZentaoIntegration|ZenTao API URL (optional)'),
- help: s_('ZentaoIntegration|If different from Web URL.')
- },
- {
- type: 'password',
- name: 'api_token',
- title: s_('ZentaoIntegration|ZenTao API token'),
- non_empty_password_title: s_('ZentaoIntegration|Enter new ZenTao API token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
- required: true
- },
- {
- type: 'text',
- name: 'zentao_product_xid',
- title: s_('ZentaoIntegration|ZenTao Product ID'),
- required: true
- }
- ]
- end
-
private
def client
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 59dd74c1363..50b6f4bbe15 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -154,6 +154,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
rule { reporter }.policy do
enable :reporter_access
enable :read_container_image
+ enable :read_harbor_registry
enable :admin_issue_board
enable :admin_label
enable :admin_milestone
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index e9266f1cb20..c54dbefc1ae 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -318,6 +318,7 @@ class ProjectPolicy < BasePolicy
enable :read_commit_status
enable :read_build
enable :read_container_image
+ enable :read_harbor_registry
enable :read_deploy_board
enable :read_pipeline
enable :read_pipeline_schedule
diff --git a/app/serializers/integrations/harbor_serializers/artifact_entity.rb b/app/serializers/integrations/harbor_serializers/artifact_entity.rb
new file mode 100644
index 00000000000..010380561eb
--- /dev/null
+++ b/app/serializers/integrations/harbor_serializers/artifact_entity.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Integrations
+ module HarborSerializers
+ class ArtifactEntity < Grape::Entity
+ include ActionView::Helpers::SanitizeHelper
+
+ expose :harbor_id do |item|
+ item['id']
+ end
+
+ expose :digest do |item|
+ strip_tags(item['digest'])
+ end
+
+ expose :size do |item|
+ item['size']
+ end
+
+ expose :push_time do |item|
+ item['push_time']&.to_datetime&.utc
+ end
+
+ expose :tags do |item|
+ item['tags'].map { |tag| strip_tags(tag['name']) }
+ end
+ end
+ end
+end
diff --git a/app/serializers/integrations/harbor_serializers/artifact_serializer.rb b/app/serializers/integrations/harbor_serializers/artifact_serializer.rb
new file mode 100644
index 00000000000..aaf78a72330
--- /dev/null
+++ b/app/serializers/integrations/harbor_serializers/artifact_serializer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Integrations
+ module HarborSerializers
+ class ArtifactSerializer < BaseSerializer
+ include WithPagination
+
+ entity ::Integrations::HarborSerializers::ArtifactEntity
+ end
+ end
+end
diff --git a/app/serializers/integrations/harbor_serializers/repository_entity.rb b/app/serializers/integrations/harbor_serializers/repository_entity.rb
new file mode 100644
index 00000000000..f03465fe8e2
--- /dev/null
+++ b/app/serializers/integrations/harbor_serializers/repository_entity.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Integrations
+ module HarborSerializers
+ class RepositoryEntity < Grape::Entity
+ include ActionView::Helpers::SanitizeHelper
+
+ expose :harbor_id do |item|
+ item['id']
+ end
+
+ expose :name do |item|
+ strip_tags(item['name'])
+ end
+
+ expose :artifact_count do |item|
+ item['artifact_count']
+ end
+
+ expose :creation_time do |item|
+ item['creation_time']&.to_datetime&.utc
+ end
+
+ expose :update_time do |item|
+ item['update_time']&.to_datetime&.utc
+ end
+
+ expose :harbor_project_id do |item|
+ item['project_id']
+ end
+
+ expose :pull_count do |item|
+ item['pull_count']
+ end
+
+ expose :location do |item|
+ path = [
+ 'harbor/projects',
+ item['project_id'].to_s,
+ 'repositories',
+ item['name'].remove("#{options[:project_name]}/")
+ ].join('/')
+ path = validate_path(path)
+ strip_tags(Gitlab::Utils.append_path(options[:url], path))
+ end
+
+ private
+
+ def validate_path(path)
+ Gitlab::Utils.check_path_traversal!(path)
+ rescue ::Gitlab::Utils::PathTraversalAttackError
+ Gitlab::AppLogger.error("Path traversal attack detected #{path}")
+ ''
+ end
+ end
+ end
+end
diff --git a/app/serializers/integrations/harbor_serializers/repository_serializer.rb b/app/serializers/integrations/harbor_serializers/repository_serializer.rb
new file mode 100644
index 00000000000..9b9e089eab8
--- /dev/null
+++ b/app/serializers/integrations/harbor_serializers/repository_serializer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Integrations
+ module HarborSerializers
+ class RepositorySerializer < BaseSerializer
+ include WithPagination
+
+ entity ::Integrations::HarborSerializers::RepositoryEntity
+ end
+ end
+end
diff --git a/app/serializers/integrations/harbor_serializers/tag_entity.rb b/app/serializers/integrations/harbor_serializers/tag_entity.rb
new file mode 100644
index 00000000000..8c26bc1ecbd
--- /dev/null
+++ b/app/serializers/integrations/harbor_serializers/tag_entity.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Integrations
+ module HarborSerializers
+ class TagEntity < Grape::Entity
+ include ActionView::Helpers::SanitizeHelper
+
+ expose :harbor_repository_id do |item|
+ item['repository_id']
+ end
+
+ expose :harbor_artifact_id do |item|
+ item['artifact_id']
+ end
+
+ expose :harbor_id do |item|
+ item['id']
+ end
+
+ expose :name do |item|
+ strip_tags(item['name'])
+ end
+
+ expose :pull_time do |item|
+ item['pull_time']&.to_datetime&.utc
+ end
+
+ expose :push_time do |item|
+ item['push_time']&.to_datetime&.utc
+ end
+
+ expose :signed do |item|
+ item['signed']
+ end
+
+ expose :immutable do |item|
+ item['immutable']
+ end
+ end
+ end
+end
diff --git a/app/serializers/integrations/harbor_serializers/tag_serializer.rb b/app/serializers/integrations/harbor_serializers/tag_serializer.rb
new file mode 100644
index 00000000000..7111e65e3e6
--- /dev/null
+++ b/app/serializers/integrations/harbor_serializers/tag_serializer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Integrations
+ module HarborSerializers
+ class TagSerializer < BaseSerializer
+ include WithPagination
+
+ entity ::Integrations::HarborSerializers::TagEntity
+ end
+ end
+end
diff --git a/app/views/groups/harbor/repositories/index.html.haml b/app/views/groups/harbor/repositories/index.html.haml
index 6a1e66520b5..a8a52b2aba7 100644
--- a/app/views/groups/harbor/repositories/index.html.haml
+++ b/app/views/groups/harbor/repositories/index.html.haml
@@ -1,7 +1,7 @@
- page_title _("Harbor Registry")
- @content_class = "limit-container-width" unless fluid_layout
-#js-harbor-registry-list-group{ data: { endpoint: group_harbor_registries_path(@group),
+#js-harbor-registry-list-group{ data: { endpoint: group_harbor_repositories_path(@group),
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"repository_url" => 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index b33d1443706..33fcda6129c 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -3,6 +3,8 @@
- page_title _("Merge requests")
- if issuables_count_for_state(:merge_requests, :all) == 0
+ = render 'shared/issuable/search_bar', type: :merge_requests
+
= render 'shared/empty_states/merge_requests', project_select_button: true
- else
.top-area
diff --git a/app/views/projects/harbor/repositories/index.html.haml b/app/views/projects/harbor/repositories/index.html.haml
index 270cbf3facd..0fce3b7f8aa 100644
--- a/app/views/projects/harbor/repositories/index.html.haml
+++ b/app/views/projects/harbor/repositories/index.html.haml
@@ -1,7 +1,7 @@
- page_title _("Harbor Registry")
- @content_class = "limit-container-width" unless fluid_layout
-#js-harbor-registry-list-project{ data: { endpoint: project_harbor_registry_index_path(@project),
+#js-harbor-registry-list-project{ data: { endpoint: project_harbor_repositories_path(@project),
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"repository_url" => 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
diff --git a/config/routes/group.rb b/config/routes/group.rb
index bf6094ff2f1..2a5931207b0 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -118,10 +118,17 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
end
resources :container_registries, only: [:index, :show], controller: 'registry/repositories'
- resources :harbor_registries, only: [:index, :show], controller: 'harbor/repositories'
resource :dependency_proxy, only: [:show, :update]
resources :email_campaigns, only: :index
+ namespace :harbor do
+ resources :repositories, only: [:index] do
+ resources :artifacts, only: [:index] do
+ resources :tags, only: [:index]
+ end
+ end
+ end
+
resources :autocomplete_sources, only: [] do
collection do
get 'members'
diff --git a/config/routes/project.rb b/config/routes/project.rb
index f996cd1945f..ba4dba47f4c 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -457,6 +457,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
end
+
+ namespace :harbor do
+ resources :repositories, only: [:index, :show] do
+ resources :artifacts, only: [:index] do
+ resources :tags, only: [:index]
+ end
+ end
+ end
end
# End of the /-/ scope.
@@ -523,9 +531,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :container_registry, only: [:index, :destroy, :show], # rubocop: disable Cop/PutProjectRoutesUnderScope
controller: 'registry/repositories'
- resources :harbor_registry, only: [:index, :show], # rubocop: disable Cop/PutProjectRoutesUnderScope
- controller: 'harbor/repositories'
-
namespace :registry do
resources :repository, only: [] do # rubocop: disable Cop/PutProjectRoutesUnderScope
# We default to JSON format in the controller to avoid ambiguity.
diff --git a/db/post_migrate/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences.rb b/db/post_migrate/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences.rb
new file mode 100644
index 00000000000..1207b51f190
--- /dev/null
+++ b/db/post_migrate/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddTmpIndexForPotentiallyMisassociatedVulnerabilityOccurrences < Gitlab::Database::Migration[2.0]
+ INDEX_NAME = "tmp_index_vulnerability_occurrences_on_id_and_scanner_id"
+ REPORT_TYPES = { cluster_image_scanning: 7, generic: 99 }.freeze
+ CLAUSE = "report_type IN (#{REPORT_TYPES.values.join(',')})"
+
+ disable_ddl_transaction!
+
+ def up
+ prepare_async_index :vulnerability_occurrences,
+ [:id, :scanner_id],
+ where: CLAUSE,
+ name: INDEX_NAME
+ end
+
+ def down
+ unprepare_async_index_by_name :vulnerability_occurrences, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20220630085003_drop_project_successfull_pages_deploy_index_from_ci_builds.rb b/db/post_migrate/20220630085003_drop_project_successfull_pages_deploy_index_from_ci_builds.rb
new file mode 100644
index 00000000000..0810419a4e8
--- /dev/null
+++ b/db/post_migrate/20220630085003_drop_project_successfull_pages_deploy_index_from_ci_builds.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class DropProjectSuccessfullPagesDeployIndexFromCiBuilds < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_ci_builds_on_project_id_for_successfull_pages_deploy'
+
+ def up
+ remove_concurrent_index_by_name :ci_builds, INDEX_NAME
+ end
+
+ # rubocop:disable Migration/PreventIndexCreation
+ def down
+ add_concurrent_index :ci_builds,
+ :project_id,
+ where: "(((type)::text = 'GenericCommitStatus'::text) AND ((stage)::text = 'deploy'::text) AND " \
+ "((name)::text = 'pages:deploy'::text) AND ((status)::text = 'success'::text))",
+ name: INDEX_NAME
+ end
+ # rubocop:enable Migration/PreventIndexCreation
+end
diff --git a/db/schema_migrations/20220606082910 b/db/schema_migrations/20220606082910
new file mode 100644
index 00000000000..5917ba95971
--- /dev/null
+++ b/db/schema_migrations/20220606082910
@@ -0,0 +1 @@
+ecab80f469d2aea061b5c8371a243e4b6686d637c3df284f23e575606ef8c1a6 \ No newline at end of file
diff --git a/db/schema_migrations/20220630085003 b/db/schema_migrations/20220630085003
new file mode 100644
index 00000000000..9e020afbe84
--- /dev/null
+++ b/db/schema_migrations/20220630085003
@@ -0,0 +1 @@
+c1fb356eb437f9511c0af324f9f4a173245a427d20e2bbda0557dfaff28911c3 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 69a5285d4a1..f4496eebb35 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -27492,8 +27492,6 @@ CREATE INDEX index_ci_builds_on_project_id_and_id ON ci_builds USING btree (proj
CREATE INDEX index_ci_builds_on_project_id_and_name_and_ref ON ci_builds USING btree (project_id, name, ref) WHERE (((type)::text = 'Ci::Build'::text) AND ((status)::text = 'success'::text) AND ((retried = false) OR (retried IS NULL)));
-CREATE INDEX index_ci_builds_on_project_id_for_successfull_pages_deploy ON ci_builds USING btree (project_id) WHERE (((type)::text = 'GenericCommitStatus'::text) AND ((stage)::text = 'deploy'::text) AND ((name)::text = 'pages:deploy'::text) AND ((status)::text = 'success'::text));
-
CREATE INDEX index_ci_builds_on_queued_at ON ci_builds USING btree (queued_at);
CREATE INDEX index_ci_builds_on_resource_group_and_status_and_commit_id ON ci_builds USING btree (resource_group_id, status, commit_id) WHERE (resource_group_id IS NOT NULL);
diff --git a/doc/administration/geo/replication/object_storage.md b/doc/administration/geo/replication/object_storage.md
index d75193b1309..d2e10678f8c 100644
--- a/doc/administration/geo/replication/object_storage.md
+++ b/doc/administration/geo/replication/object_storage.md
@@ -34,8 +34,7 @@ See [Object storage replication tests](geo_validation_tests.md#object-storage-re
## Enabling GitLab-managed object storage replication
-> The beta feature was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10586) in GitLab 12.4.
-> The feature was made [generally available] in GitLab 15.1.
+> The feature was made [generally available](https://gitlab.com/groups/gitlab-org/-/epics/5551) in GitLab 15.1.
**Secondary** sites can replicate files stored on the **primary** site regardless of
whether they are stored on the local file system or in object storage.
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 7b7ceb1e986..e33f3990ddd 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -381,7 +381,7 @@ listed in the descriptions of the relevant settings.
| `metrics_method_call_threshold` | integer | no | A method call is only tracked when it takes longer than the given amount of milliseconds. |
| `max_number_of_repository_downloads` **(ULTIMATE SELF)** | integer | no | Maximum number of unique repositories a user can download in the specified time period before they are banned. Default: 0, Maximum: 10,000 repositories. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87980) in GitLab 15.1. |
| `max_number_of_repository_downloads_within_time_period` **(ULTIMATE SELF)** | integer | no | Reporting time period (in seconds). Default: 0, Maximum: 864000 seconds (10 days). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87980) in GitLab 15.1. |
-| `git_rate_limit_users_allowlist` **(ULTIMATE SELF)** | array of strings | no | List of usernames excluded from Git anti-abuse rate limits. Default: [], Maximum: 100 usernames. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90815) in GitLab 15.2. |
+| `git_rate_limit_users_allowlist` **(ULTIMATE SELF)** | array of strings | no | List of usernames excluded from Git anti-abuse rate limits. Default: `[]`, Maximum: 100 usernames. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90815) in GitLab 15.2. |
| `mirror_available` | boolean | no | Allow repository mirroring to configured by project Maintainers. If disabled, only Administrators can configure repository mirroring. |
| `mirror_capacity_threshold` **(PREMIUM)** | integer | no | Minimum capacity to be available before scheduling more mirrors preemptively. |
| `mirror_max_capacity` **(PREMIUM)** | integer | no | Maximum number of mirrors that can be synchronizing at the same time. |
diff --git a/doc/user/infrastructure/iac/terraform_state.md b/doc/user/infrastructure/iac/terraform_state.md
index e29228c7a4c..24203e8d922 100644
--- a/doc/user/infrastructure/iac/terraform_state.md
+++ b/doc/user/infrastructure/iac/terraform_state.md
@@ -22,6 +22,16 @@ In GitLab, you can:
- Lock and unlock states.
- Remotely execute `terraform plan` and `terraform apply` commands.
+WARNING:
+**Disaster recovery planning**
+Terraform state files are encrypted with the lockbox Ruby gem when they are at rest on disk and in object storage.
+[To decrypt a state file, GitLab must be available](https://gitlab.com/gitlab-org/gitlab/-/issues/335739).
+If it is offline, and you use GitLab to deploy infrastructure that GitLab requires (like virtual machines,
+Kubernetes clusters, or network components), you cannot access the state file easily or decrypt it.
+Additionally, if GitLab serves up Terraform modules or other dependencies that are required to bootstrap GitLab,
+these will be inaccessible. To work around this issue, make other arrangements to host or back up these dependencies,
+or consider using a separate GitLab instance with no shared points of failure.
+
## Prerequisites
For self-managed GitLab, before you can use GitLab for your Terraform state files:
diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb
index 0fbd0e6be44..0b0100c7d7f 100644
--- a/lib/api/helpers/integrations_helpers.rb
+++ b/lib/api/helpers/integrations_helpers.rb
@@ -633,6 +633,12 @@ module API
},
{
required: false,
+ name: :notify_only_default_branch,
+ type: Boolean,
+ desc: 'Send notifications only for the default branch'
+ },
+ {
+ required: false,
name: :branches_to_be_notified,
type: String,
desc: 'Branches for which notifications are to be sent'
diff --git a/lib/gitlab/harbor/client.rb b/lib/gitlab/harbor/client.rb
index 06142ae2b40..ee40725ba95 100644
--- a/lib/gitlab/harbor/client.rb
+++ b/lib/gitlab/harbor/client.rb
@@ -21,14 +21,44 @@ module Gitlab
{ success: response.success? }
end
+ def get_repositories(params)
+ get(url("projects/#{integration.project_name}/repositories"), params)
+ end
+
+ def get_artifacts(params)
+ repository_name = params.delete(:repository_name)
+ get(url("projects/#{integration.project_name}/repositories/#{repository_name}/artifacts"), params)
+ end
+
+ def get_tags(params)
+ repository_name = params.delete(:repository_name)
+ artifact_name = params.delete(:artifact_name)
+ get(
+ url("projects/#{integration.project_name}/repositories/#{repository_name}/artifacts/#{artifact_name}/tags"),
+ params
+ )
+ end
+
private
- def url(path)
- Gitlab::Utils.append_path(base_url, path)
+ def get(path, params = {})
+ options = { headers: headers, query: params }
+ response = Gitlab::HTTP.get(path, options)
+
+ raise Gitlab::Harbor::Client::Error, 'request error' unless response.success?
+
+ {
+ body: Gitlab::Json.parse(response.body),
+ total_count: response.headers['x-total-count'].to_i
+ }
+ rescue JSON::ParserError
+ raise Gitlab::Harbor::Client::Error, 'invalid response format'
end
- def base_url
- Gitlab::Utils.append_path(integration.url, '/api/v2.0/')
+ # url must be used within get method otherwise this would avoid validation by GitLab::HTTP
+ def url(path)
+ base_url = Gitlab::Utils.append_path(integration.url, '/api/v2.0/')
+ Gitlab::Utils.append_path(base_url, path)
end
def headers
diff --git a/lib/gitlab/harbor/query.rb b/lib/gitlab/harbor/query.rb
new file mode 100644
index 00000000000..c120810ecf1
--- /dev/null
+++ b/lib/gitlab/harbor/query.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Harbor
+ class Query
+ include ActiveModel::Validations
+
+ attr_reader :client, :repository_id, :artifact_id, :search, :limit, :sort, :page
+
+ DEFAULT_LIMIT = 10
+ SORT_REGEX = %r{\A(creation_time|update_time|name) (asc|desc)\z}.freeze
+
+ validates :page, numericality: { greater_than: 0, integer: true }, allow_blank: true
+ validates :limit, numericality: { greater_than: 0, less_than_or_equal_to: 25, integer: true }, allow_blank: true
+ validates :repository_id, format: {
+ with: /\A[a-zA-Z0-9\_\.\-$]+\z/,
+ message: 'Id invalid'
+ }, allow_blank: true
+ validates :artifact_id, format: {
+ with: /\A[a-zA-Z0-9\_\.\-$]+\z/,
+ message: 'Id invalid'
+ }, allow_blank: true
+ validates :sort, format: {
+ with: SORT_REGEX,
+ message: 'params invalid'
+ }, allow_blank: true
+ validates :search, format: {
+ with: /\A([a-z\_]*=[a-zA-Z0-9\- :]*,*)*\z/,
+ message: 'params invalid'
+ }, allow_blank: true
+
+ def initialize(integration, params)
+ @client = Client.new(integration)
+ @repository_id = params[:repository_id]
+ @artifact_id = params[:artifact_id]
+ @search = params[:search]
+ @limit = params[:limit]
+ @sort = params[:sort]
+ @page = params[:page]
+ validate
+ end
+
+ def repositories
+ result = @client.get_repositories(query_options)
+ return [] if result[:total_count] == 0
+
+ Kaminari.paginate_array(
+ result[:body],
+ limit: query_page_size,
+ total_count: result[:total_count]
+ )
+ end
+
+ def artifacts
+ result = @client.get_artifacts(query_artifacts_options)
+ return [] if result[:total_count] == 0
+
+ Kaminari.paginate_array(
+ result[:body],
+ limit: query_page_size,
+ total_count: result[:total_count]
+ )
+ end
+
+ def tags
+ result = @client.get_tags(query_tags_options)
+ return [] if result[:total_count] == 0
+
+ Kaminari.paginate_array(
+ result[:body],
+ limit: query_page_size,
+ total_count: result[:total_count]
+ )
+ end
+
+ private
+
+ def query_artifacts_options
+ options = query_options
+ options[:repository_name] = repository_id
+ options[:with_tag] = true
+
+ options
+ end
+
+ def query_options
+ options = {
+ page: query_page,
+ page_size: query_page_size
+ }
+
+ options[:q] = query_search if search.present?
+ options[:sort] = query_sort if sort.present?
+
+ options
+ end
+
+ def query_tags_options
+ options = query_options
+ options[:repository_name] = repository_id
+ options[:artifact_name] = artifact_id
+
+ options
+ end
+
+ def query_page
+ page.presence || 1
+ end
+
+ def query_page_size
+ (limit.presence || DEFAULT_LIMIT).to_i
+ end
+
+ def query_search
+ search.gsub('=', '=~')
+ end
+
+ def query_sort
+ match = sort.match(SORT_REGEX)
+ order = (match[2] == 'asc' ? '' : '-')
+
+ "#{order}#{match[1]}"
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/groups/menus/packages_registries_menu.rb b/lib/sidebars/groups/menus/packages_registries_menu.rb
index 4c21845ef18..fda90406e0a 100644
--- a/lib/sidebars/groups/menus/packages_registries_menu.rb
+++ b/lib/sidebars/groups/menus/packages_registries_menu.rb
@@ -54,7 +54,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Harbor Registry'),
- link: group_harbor_registries_path(context.group),
+ link: group_harbor_repositories_path(context.group),
active_routes: { controller: 'groups/harbor/repositories' },
item_id: :harbor_registry
)
diff --git a/lib/sidebars/projects/menus/packages_registries_menu.rb b/lib/sidebars/projects/menus/packages_registries_menu.rb
index d82a02a342f..914368e6fec 100644
--- a/lib/sidebars/projects/menus/packages_registries_menu.rb
+++ b/lib/sidebars/projects/menus/packages_registries_menu.rb
@@ -70,8 +70,8 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Harbor Registry'),
- link: project_harbor_registry_index_path(context.project),
- active_routes: { controller: 'projects/harbor/repositories' },
+ link: project_harbor_repositories_path(context.project),
+ active_routes: { controller: :harbor_registry },
item_id: :harbor_registry
)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index abba06393da..6f1034113a0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -21698,15 +21698,9 @@ msgstr ""
msgid "IssueTracker|The URL to create an issue in the external issue tracker."
msgstr ""
-msgid "IssueTracker|The URL to the project in YouTrack."
-msgstr ""
-
msgid "IssueTracker|The URL to the project in the external issue tracker."
msgstr ""
-msgid "IssueTracker|The URL to view an issue in the YouTrack project. Must contain %{colon_id}."
-msgstr ""
-
msgid "IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}."
msgstr ""
@@ -30221,9 +30215,6 @@ msgstr ""
msgid "ProjectService|Enter new token"
msgstr ""
-msgid "ProjectService|Issue URL"
-msgstr ""
-
msgid "ProjectService|Jenkins server URL"
msgstr ""
diff --git a/spec/controllers/concerns/harbor/artifact_spec.rb b/spec/controllers/concerns/harbor/artifact_spec.rb
new file mode 100644
index 00000000000..6716d615a3b
--- /dev/null
+++ b/spec/controllers/concerns/harbor/artifact_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Harbor::Artifact do
+ controller(ActionController::Base) do
+ include ::Harbor::Artifact
+ end
+ it_behaves_like 'raises NotImplementedError when calling #container'
+end
diff --git a/spec/controllers/concerns/harbor/repository_spec.rb b/spec/controllers/concerns/harbor/repository_spec.rb
new file mode 100644
index 00000000000..cae038ceed2
--- /dev/null
+++ b/spec/controllers/concerns/harbor/repository_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Harbor::Repository do
+ controller(ActionController::Base) do
+ include ::Harbor::Repository
+ end
+ it_behaves_like 'raises NotImplementedError when calling #container'
+end
diff --git a/spec/controllers/concerns/harbor/tag_spec.rb b/spec/controllers/concerns/harbor/tag_spec.rb
new file mode 100644
index 00000000000..0d72ef303b0
--- /dev/null
+++ b/spec/controllers/concerns/harbor/tag_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Harbor::Tag do
+ controller(ActionController::Base) do
+ include ::Harbor::Tag
+ end
+ it_behaves_like 'raises NotImplementedError when calling #container'
+end
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index 3945637c2c3..5ac26b7a260 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -233,7 +233,7 @@ FactoryBot.define do
factory :harbor_integration, class: 'Integrations::Harbor' do
project
active { true }
- type { 'HarborService' }
+ type { 'Integrations::Harbor' }
url { 'https://demo.goharbor.io' }
project_name { 'testproject' }
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 7541e54f014..be1db970e9d 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe 'Group merge requests page' do
expect(page).to have_selector('.empty-state')
expect(page).to have_link('Select project to create merge request')
- expect(page).not_to have_selector('.issues-filters')
+ expect(page).to have_selector('.issues-filters')
end
context 'with no open merge requests' do
diff --git a/spec/lib/gitlab/harbor/client_spec.rb b/spec/lib/gitlab/harbor/client_spec.rb
index bc5b593370a..4e80b8b53e3 100644
--- a/spec/lib/gitlab/harbor/client_spec.rb
+++ b/spec/lib/gitlab/harbor/client_spec.rb
@@ -3,12 +3,277 @@
require 'spec_helper'
RSpec.describe Gitlab::Harbor::Client do
- let(:harbor_integration) { build(:harbor_integration) }
+ let_it_be(:harbor_integration) { create(:harbor_integration) }
subject(:client) { described_class.new(harbor_integration) }
+ describe '#initialize' do
+ context 'if integration is nil' do
+ let(:harbor_integration) { nil }
+
+ it 'raises ConfigError' do
+ expect { client }.to raise_error(described_class::ConfigError)
+ end
+ end
+
+ context 'integration is provided' do
+ it 'is initialized successfully' do
+ expect { client }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#get_repositories' do
+ context 'with valid params' do
+ let(:mock_response) do
+ [
+ {
+ "artifact_count": 1,
+ "creation_time": "2022-03-13T09:36:43.240Z",
+ "id": 1,
+ "name": "jihuprivate/busybox",
+ "project_id": 4,
+ "pull_count": 0,
+ "update_time": "2022-03-13T09:36:43.240Z"
+ }
+ ]
+ end
+
+ let(:mock_repositories) do
+ {
+ body: mock_response,
+ total_count: 2
+ }
+ end
+
+ before do
+ stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ })
+ .to_return(status: 200, body: mock_response.to_json, headers: { "x-total-count": 2 })
+ end
+
+ it 'get repositories' do
+ expect(client.get_repositories({}).deep_stringify_keys).to eq(mock_repositories.deep_stringify_keys)
+ end
+ end
+
+ context 'when harbor project does not exist' do
+ before do
+ stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ })
+ .to_return(status: 404, body: {}.to_json)
+ end
+
+ it 'raises Gitlab::Harbor::Client::Error' do
+ expect do
+ client.get_repositories({})
+ end.to raise_error(Gitlab::Harbor::Client::Error, 'request error')
+ end
+ end
+
+ context 'with invalid response' do
+ before do
+ stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ })
+ .to_return(status: 200, body: '[not json}')
+ end
+
+ it 'raises Gitlab::Harbor::Client::Error' do
+ expect do
+ client.get_repositories({})
+ end.to raise_error(Gitlab::Harbor::Client::Error, 'invalid response format')
+ end
+ end
+ end
+
+ describe '#get_artifacts' do
+ context 'with valid params' do
+ let(:mock_response) do
+ [
+ {
+ "digest": "sha256:661e8e44e5d7290fbd42d0495ab4ff6fdf1ad251a9f358969b3264a22107c14d",
+ "icon": "sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06",
+ "id": 1,
+ "project_id": 1,
+ "pull_time": "0001-01-01T00:00:00.000Z",
+ "push_time": "2022-04-23T08:04:08.901Z",
+ "repository_id": 1,
+ "size": 126745886,
+ "tags": [
+ {
+ "artifact_id": 1,
+ "id": 1,
+ "immutable": false,
+ "name": "2",
+ "pull_time": "0001-01-01T00:00:00.000Z",
+ "push_time": "2022-04-23T08:04:08.920Z",
+ "repository_id": 1,
+ "signed": false
+ }
+ ],
+ "type": "IMAGE"
+ }
+ ]
+ end
+
+ let(:mock_artifacts) do
+ {
+ body: mock_response,
+ total_count: 1
+ }
+ end
+
+ before do
+ stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ })
+ .to_return(status: 200, body: mock_response.to_json, headers: { "x-total-count": 1 })
+ end
+
+ it 'get artifacts' do
+ expect(client.get_artifacts({ repository_name: 'test' })
+ .deep_stringify_keys).to eq(mock_artifacts.deep_stringify_keys)
+ end
+ end
+
+ context 'when harbor repository does not exist' do
+ before do
+ stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ })
+ .to_return(status: 404, body: {}.to_json)
+ end
+
+ it 'raises Gitlab::Harbor::Client::Error' do
+ expect do
+ client.get_artifacts({ repository_name: 'test' })
+ end.to raise_error(Gitlab::Harbor::Client::Error, 'request error')
+ end
+ end
+
+ context 'with invalid response' do
+ before do
+ stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ })
+ .to_return(status: 200, body: '[not json}')
+ end
+
+ it 'raises Gitlab::Harbor::Client::Error' do
+ expect do
+ client.get_artifacts({ repository_name: 'test' })
+ end.to raise_error(Gitlab::Harbor::Client::Error, 'invalid response format')
+ end
+ end
+ end
+
+ describe '#get_tags' do
+ context 'with valid params' do
+ let(:mock_response) do
+ [
+ {
+ "artifact_id": 1,
+ "id": 1,
+ "immutable": false,
+ "name": "2",
+ "pull_time": "0001-01-01T00:00:00.000Z",
+ "push_time": "2022-04-23T08:04:08.920Z",
+ "repository_id": 1,
+ "signed": false
+ }
+ ]
+ end
+
+ let(:mock_tags) do
+ {
+ body: mock_response,
+ total_count: 1
+ }
+ end
+
+ before do
+ stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts/1/tags")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ })
+ .to_return(status: 200, body: mock_response.to_json, headers: { "x-total-count": 1 })
+ end
+
+ it 'get tags' do
+ expect(client.get_tags({ repository_name: 'test', artifact_name: '1' })
+ .deep_stringify_keys).to eq(mock_tags.deep_stringify_keys)
+ end
+ end
+
+ context 'when harbor artifact does not exist' do
+ before do
+ stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts/1/tags")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ })
+ .to_return(status: 404, body: {}.to_json)
+ end
+
+ it 'raises Gitlab::Harbor::Client::Error' do
+ expect do
+ client.get_tags({ repository_name: 'test', artifact_name: '1' })
+ end.to raise_error(Gitlab::Harbor::Client::Error, 'request error')
+ end
+ end
+
+ context 'with invalid response' do
+ before do
+ stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts/1/tags")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ })
+ .to_return(status: 200, body: '[not json}')
+ end
+
+ it 'raises Gitlab::Harbor::Client::Error' do
+ expect do
+ client.get_tags({ repository_name: 'test', artifact_name: '1' })
+ end.to raise_error(Gitlab::Harbor::Client::Error, 'invalid response format')
+ end
+ end
+ end
+
describe '#ping' do
- let!(:harbor_ping_request) { stub_harbor_request("https://demo.goharbor.io/api/v2.0/ping") }
+ before do
+ stub_request(:get, "https://demo.goharbor.io/api/v2.0/ping")
+ .with(
+ headers: {
+ 'Content-Type': 'application/json'
+ })
+ .to_return(status: 200, body: 'pong')
+ end
it "calls api/v2.0/ping successfully" do
expect(client.ping).to eq(success: true)
diff --git a/spec/lib/gitlab/harbor/query_spec.rb b/spec/lib/gitlab/harbor/query_spec.rb
new file mode 100644
index 00000000000..dcb9a16b27b
--- /dev/null
+++ b/spec/lib/gitlab/harbor/query_spec.rb
@@ -0,0 +1,375 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Harbor::Query do
+ let_it_be(:harbor_integration) { create(:harbor_integration) }
+
+ let(:params) { {} }
+
+ subject(:query) { described_class.new(harbor_integration, ActionController::Parameters.new(params)) }
+
+ describe 'Validations' do
+ context 'page' do
+ context 'with valid page' do
+ let(:params) { { page: 1 } }
+
+ it 'initialize successfully' do
+ expect(query.valid?).to eq(true)
+ end
+ end
+
+ context 'with invalid page' do
+ let(:params) { { page: -1 } }
+
+ it 'initialize failed' do
+ expect(query.valid?).to eq(false)
+ end
+ end
+ end
+
+ context 'limit' do
+ context 'with valid limit' do
+ let(:params) { { limit: 1 } }
+
+ it 'initialize successfully' do
+ expect(query.valid?).to eq(true)
+ end
+ end
+
+ context 'with invalid limit' do
+ context 'with limit less than 0' do
+ let(:params) { { limit: -1 } }
+
+ it 'initialize failed' do
+ expect(query.valid?).to eq(false)
+ end
+ end
+
+ context 'with limit greater than 25' do
+ let(:params) { { limit: 26 } }
+
+ it 'initialize failed' do
+ expect(query.valid?).to eq(false)
+ end
+ end
+ end
+ end
+
+ context 'repository_id' do
+ context 'with valid repository_id' do
+ let(:params) { { repository_id: 'test' } }
+
+ it 'initialize successfully' do
+ expect(query.valid?).to eq(true)
+ end
+ end
+
+ context 'with invalid repository_id' do
+ let(:params) { { repository_id: 'test@@' } }
+
+ it 'initialize failed' do
+ expect(query.valid?).to eq(false)
+ end
+ end
+ end
+
+ context 'artifact_id' do
+ context 'with valid artifact_id' do
+ let(:params) { { artifact_id: 'test' } }
+
+ it 'initialize successfully' do
+ expect(query.valid?).to eq(true)
+ end
+ end
+
+ context 'with invalid artifact_id' do
+ let(:params) { { artifact_id: 'test@@' } }
+
+ it 'initialize failed' do
+ expect(query.valid?).to eq(false)
+ end
+ end
+ end
+
+ context 'sort' do
+ context 'with valid sort' do
+ let(:params) { { sort: 'creation_time desc' } }
+
+ it 'initialize successfully' do
+ expect(query.valid?).to eq(true)
+ end
+ end
+
+ context 'with invalid sort' do
+ let(:params) { { sort: 'blabla desc' } }
+
+ it 'initialize failed' do
+ expect(query.valid?).to eq(false)
+ end
+ end
+ end
+
+ context 'search' do
+ context 'with valid search' do
+ let(:params) { { search: 'name=desc' } }
+
+ it 'initialize successfully' do
+ expect(query.valid?).to eq(true)
+ end
+ end
+
+ context 'with invalid search' do
+ let(:params) { { search: 'blabla' } }
+
+ it 'initialize failed' do
+ expect(query.valid?).to eq(false)
+ end
+ end
+ end
+ end
+
+ describe '#repositories' do
+ let(:response) { { total_count: 0, repositories: [] } }
+
+ def expect_query_option_include(expected_params)
+ expect_next_instance_of(Gitlab::Harbor::Client) do |client|
+ expect(client).to receive(:get_repositories)
+ .with(hash_including(expected_params))
+ .and_return(response)
+ end
+
+ query.repositories
+ end
+
+ context 'when params is {}' do
+ it 'fills default params' do
+ expect_query_option_include(page_size: 10, page: 1)
+ end
+ end
+
+ context 'when params contains options' do
+ let(:params) { { search: 'name=bu', sort: 'creation_time desc', limit: 20, page: 3 } }
+
+ it 'fills params with standard of Harbor' do
+ expect_query_option_include(q: 'name=~bu', sort: '-creation_time', page_size: 20, page: 3)
+ end
+ end
+
+ context 'when params contains invalid sort option' do
+ let(:params) { { search: 'name=bu', sort: 'blabla desc', limit: 20, page: 3 } }
+
+ it 'ignores invalid sort params' do
+ expect(query.valid?).to eq(false)
+ end
+ end
+
+ context 'when client.get_repositories returns data' do
+ let(:response_with_data) do
+ {
+ total_count: 1,
+ body:
+ [
+ {
+ "id": 3,
+ "name": "testproject/thirdbusybox",
+ "artifact_count": 1,
+ "creation_time": "2022-03-15T07:12:14.479Z",
+ "update_time": "2022-03-15T07:12:14.479Z",
+ "project_id": 3,
+ "pull_count": 0
+ }.with_indifferent_access
+ ]
+ }
+ end
+
+ it 'returns the right repositories data' do
+ expect_next_instance_of(Gitlab::Harbor::Client) do |client|
+ expect(client).to receive(:get_repositories)
+ .with(hash_including(page_size: 10, page: 1))
+ .and_return(response_with_data)
+ end
+
+ expect(query.repositories.first).to include(
+ "name": "testproject/thirdbusybox",
+ "artifact_count": 1
+ )
+ end
+ end
+ end
+
+ describe '#artifacts' do
+ let(:response) { { total_count: 0, artifacts: [] } }
+
+ def expect_query_option_include(expected_params)
+ expect_next_instance_of(Gitlab::Harbor::Client) do |client|
+ expect(client).to receive(:get_artifacts)
+ .with(hash_including(expected_params))
+ .and_return(response)
+ end
+
+ query.artifacts
+ end
+
+ context 'when params is {}' do
+ it 'fills default params' do
+ expect_query_option_include(page_size: 10, page: 1)
+ end
+ end
+
+ context 'when params contains options' do
+ let(:params) do
+ { search: 'tags=1', repository_id: 'jihuprivate', sort: 'creation_time desc', limit: 20, page: 3 }
+ end
+
+ it 'fills params with standard of Harbor' do
+ expect_query_option_include(q: 'tags=~1', sort: '-creation_time', page_size: 20, page: 3)
+ end
+ end
+
+ context 'when params contains invalid sort option' do
+ let(:params) { { search: 'tags=1', repository_id: 'jihuprivate', sort: 'blabla desc', limit: 20, page: 3 } }
+
+ it 'ignores invalid sort params' do
+ expect(query.valid?).to eq(false)
+ end
+ end
+
+ context 'when client.get_artifacts returns data' do
+ let(:response_with_data) do
+ {
+ total_count: 1,
+ body:
+ [
+ {
+ "digest": "sha256:14d4f50961544fdb669075c442509f194bdc4c0e344bde06e35dbd55af842a38",
+ "icon": "sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06",
+ "id": 5,
+ "project_id": 14,
+ "push_time": "2022-03-22T09:04:56.170Z",
+ "repository_id": 5,
+ "size": 774790,
+ "tags": [
+ {
+ "artifact_id": 5,
+ "id": 7,
+ "immutable": false,
+ "name": "2",
+ "pull_time": "0001-01-01T00:00:00.000Z",
+ "push_time": "2022-03-22T09:05:04.844Z",
+ "repository_id": 5
+ },
+ {
+ "artifact_id": 5,
+ "id": 6,
+ "immutable": false,
+ "name": "1",
+ "pull_time": "0001-01-01T00:00:00.000Z",
+ "push_time": "2022-03-22T09:04:56.186Z",
+ "repository_id": 5
+ }
+ ],
+ "type": "IMAGE"
+ }.with_indifferent_access
+ ]
+ }
+ end
+
+ it 'returns the right artifacts data' do
+ expect_next_instance_of(Gitlab::Harbor::Client) do |client|
+ expect(client).to receive(:get_artifacts)
+ .with(hash_including(page_size: 10, page: 1))
+ .and_return(response_with_data)
+ end
+
+ artifact = query.artifacts.first
+
+ expect(artifact).to include(
+ "digest": "sha256:14d4f50961544fdb669075c442509f194bdc4c0e344bde06e35dbd55af842a38",
+ "push_time": "2022-03-22T09:04:56.170Z"
+ )
+ expect(artifact["tags"].size).to eq(2)
+ end
+ end
+ end
+
+ describe '#tags' do
+ let(:response) { { total_count: 0, tags: [] } }
+
+ def expect_query_option_include(expected_params)
+ expect_next_instance_of(Gitlab::Harbor::Client) do |client|
+ expect(client).to receive(:get_tags)
+ .with(hash_including(expected_params))
+ .and_return(response)
+ end
+
+ query.tags
+ end
+
+ context 'when params is {}' do
+ it 'fills default params' do
+ expect_query_option_include(page_size: 10, page: 1)
+ end
+ end
+
+ context 'when params contains options' do
+ let(:params) { { repository_id: 'jihuprivate', sort: 'creation_time desc', limit: 20, page: 3 } }
+
+ it 'fills params with standard of Harbor' do
+ expect_query_option_include(sort: '-creation_time', page_size: 20, page: 3)
+ end
+ end
+
+ context 'when params contains invalid sort option' do
+ let(:params) { { repository_id: 'jihuprivate', artifact_id: 'test', sort: 'blabla desc', limit: 20, page: 3 } }
+
+ it 'ignores invalid sort params' do
+ expect(query.valid?).to eq(false)
+ end
+ end
+
+ context 'when client.get_tags returns data' do
+ let(:response_with_data) do
+ {
+ total_count: 2,
+ body:
+ [
+ {
+ "artifact_id": 5,
+ "id": 7,
+ "immutable": false,
+ "name": "2",
+ "pull_time": "0001-01-01T00:00:00.000Z",
+ "push_time": "2022-03-22T09:05:04.844Z",
+ "repository_id": 5
+ },
+ {
+ "artifact_id": 5,
+ "id": 6,
+ "immutable": false,
+ "name": "1",
+ "pull_time": "0001-01-01T00:00:00.000Z",
+ "push_time": "2022-03-22T09:04:56.186Z",
+ "repository_id": 5
+ }.with_indifferent_access
+ ]
+ }
+ end
+
+ it 'returns the right tags data' do
+ expect_next_instance_of(Gitlab::Harbor::Client) do |client|
+ expect(client).to receive(:get_tags)
+ .with(hash_including(page_size: 10, page: 1))
+ .and_return(response_with_data)
+ end
+
+ tag = query.tags.first
+
+ expect(tag).to include(
+ "immutable": false,
+ "push_time": "2022-03-22T09:05:04.844Z"
+ )
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb b/spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb
new file mode 100644
index 00000000000..1450811b3b9
--- /dev/null
+++ b/spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+require_migration!
+
+RSpec.describe AddTmpIndexForPotentiallyMisassociatedVulnerabilityOccurrences do
+ let(:async_index) { Gitlab::Database::AsyncIndexes::PostgresAsyncIndex }
+ let(:index_name) { described_class::INDEX_NAME }
+
+ it "schedules the index" do
+ reversible_migration do |migration|
+ migration.before -> do
+ expect(async_index.where(name: index_name).count).to be(0)
+ end
+
+ migration.after -> do
+ expect(async_index.where(name: index_name).count).to be(1)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
index 3a4b836e453..fc5a9c879f6 100644
--- a/spec/models/ci/group_variable_spec.rb
+++ b/spec/models/ci/group_variable_spec.rb
@@ -56,4 +56,10 @@ RSpec.describe Ci::GroupVariable do
let!(:parent) { model.group }
end
+
+ describe '#audit_details' do
+ it "equals to the group variable's key" do
+ expect(subject.audit_details).to eq(subject.key)
+ end
+ end
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 29ca088ee04..f0af229ff2c 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -51,4 +51,10 @@ RSpec.describe Ci::Variable do
let!(:model) { create(:ci_variable, project: parent) }
end
end
+
+ describe '#audit_details' do
+ it "equals to the variable's key" do
+ expect(subject.audit_details).to eq(subject.key)
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 91ba12a16d5..e8e805b2678 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -42,6 +42,7 @@ RSpec.describe Group do
it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
it { is_expected.to have_one(:crm_settings) }
it { is_expected.to have_one(:group_feature) }
+ it { is_expected.to have_one(:harbor_integration) }
describe '#members & #requesters' do
let(:requester) { create(:user) }
diff --git a/spec/models/integrations/datadog_spec.rb b/spec/models/integrations/datadog_spec.rb
index cfc44b22a84..47f916e8457 100644
--- a/spec/models/integrations/datadog_spec.rb
+++ b/spec/models/integrations/datadog_spec.rb
@@ -240,4 +240,20 @@ RSpec.describe Integrations::Datadog do
end
end
end
+
+ describe '#fields' do
+ it 'includes the archive_trace_events field' do
+ expect(instance.fields).to include(have_attributes(name: 'archive_trace_events'))
+ end
+
+ context 'when the FF :datadog_integration_logs_collection is disabled' do
+ before do
+ stub_feature_flags(datadog_integration_logs_collection: false)
+ end
+
+ it 'does not include the archive_trace_events field' do
+ expect(instance.fields).not_to include(have_attributes(name: 'archive_trace_events'))
+ end
+ end
+ end
end
diff --git a/spec/models/integrations/harbor_spec.rb b/spec/models/integrations/harbor_spec.rb
index 9e3d4b524a6..5d8597969a1 100644
--- a/spec/models/integrations/harbor_spec.rb
+++ b/spec/models/integrations/harbor_spec.rb
@@ -19,6 +19,14 @@ RSpec.describe Integrations::Harbor do
it { is_expected.to allow_value('helloworld').for(:password) }
end
+ describe 'url' do
+ subject { build(:harbor_integration) }
+
+ it { is_expected.not_to allow_value('https://192.168.1.1').for(:url) }
+ it { is_expected.not_to allow_value('https://127.0.0.1').for(:url) }
+ it { is_expected.to allow_value('https://demo.goharbor.io').for(:url)}
+ end
+
describe '#fields' do
it 'returns custom fields' do
expect(harbor_integration.fields.pluck(:name)).to eq(%w[url project_name username password])
diff --git a/spec/models/integrations/youtrack_spec.rb b/spec/models/integrations/youtrack_spec.rb
index f6a9dd8ef37..618ebcbb76a 100644
--- a/spec/models/integrations/youtrack_spec.rb
+++ b/spec/models/integrations/youtrack_spec.rb
@@ -37,4 +37,10 @@ RSpec.describe Integrations::Youtrack do
expect(described_class.reference_pattern.match('yt-123')[:issue]).to eq('yt-123')
end
end
+
+ describe '#fields' do
+ it 'only returns the project_url and issues_url fields' do
+ expect(subject.fields.pluck(:name)).to eq(%w[project_url issues_url])
+ end
+ end
end
diff --git a/spec/requests/groups/harbor/artifacts_controller_spec.rb b/spec/requests/groups/harbor/artifacts_controller_spec.rb
new file mode 100644
index 00000000000..ea9529119a6
--- /dev/null
+++ b/spec/requests/groups/harbor/artifacts_controller_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::Harbor::ArtifactsController do
+ it_behaves_like 'a harbor artifacts controller', anonymous_status_code: '404' do
+ let_it_be(:container) { create(:group) }
+ let_it_be(:harbor_integration) { create(:harbor_integration, group: container, project: nil) }
+ end
+end
diff --git a/spec/requests/groups/harbor/repositories_controller_spec.rb b/spec/requests/groups/harbor/repositories_controller_spec.rb
index 3e475dc410e..b4022561f54 100644
--- a/spec/requests/groups/harbor/repositories_controller_spec.rb
+++ b/spec/requests/groups/harbor/repositories_controller_spec.rb
@@ -3,67 +3,8 @@
require 'spec_helper'
RSpec.describe Groups::Harbor::RepositoriesController do
- let_it_be(:group, reload: true) { create(:group) }
- let_it_be(:user) { create(:user) }
-
- shared_examples 'responds with 404 status' do
- it 'returns 404' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- shared_examples 'responds with 200 status' do
- it 'renders the index template' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:index)
- end
- end
-
- before do
- stub_feature_flags(harbor_registry_integration: true)
- group.add_reporter(user)
- login_as(user)
- end
-
- describe 'GET #index' do
- subject do
- get group_harbor_registries_path(group)
- response
- end
-
- context 'with harbor registry feature flag enabled' do
- it_behaves_like 'responds with 200 status'
- end
-
- context 'with harbor registry feature flag disabled' do
- before do
- stub_feature_flags(harbor_registry_integration: false)
- end
-
- it_behaves_like 'responds with 404 status'
- end
- end
-
- describe 'GET #show' do
- subject do
- get group_harbor_registry_path(group, 1)
- response
- end
-
- context 'with harbor registry feature flag enabled' do
- it_behaves_like 'responds with 200 status'
- end
-
- context 'with harbor registry feature flag disabled' do
- before do
- stub_feature_flags(harbor_registry_integration: false)
- end
-
- it_behaves_like 'responds with 404 status'
- end
+ it_behaves_like 'a harbor repositories controller', anonymous_status_code: '404' do
+ let_it_be(:container, reload: true) { create(:group) }
+ let_it_be(:harbor_integration) { create(:harbor_integration, group: container, project: nil) }
end
end
diff --git a/spec/requests/groups/harbor/tags_controller_spec.rb b/spec/requests/groups/harbor/tags_controller_spec.rb
new file mode 100644
index 00000000000..257d4366837
--- /dev/null
+++ b/spec/requests/groups/harbor/tags_controller_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::Harbor::TagsController do
+ it_behaves_like 'a harbor tags controller', anonymous_status_code: '404' do
+ let_it_be(:container) { create(:group) }
+ let_it_be(:harbor_integration) { create(:harbor_integration, group: container, project: nil) }
+ end
+end
diff --git a/spec/requests/projects/harbor/artifacts_controller_spec.rb b/spec/requests/projects/harbor/artifacts_controller_spec.rb
new file mode 100644
index 00000000000..310fbcf0a0f
--- /dev/null
+++ b/spec/requests/projects/harbor/artifacts_controller_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Harbor::ArtifactsController do
+ it_behaves_like 'a harbor artifacts controller', anonymous_status_code: '302' do
+ let_it_be(:container) { create(:project) }
+ let_it_be(:harbor_integration) { create(:harbor_integration, project: container) }
+ end
+end
diff --git a/spec/requests/projects/harbor/repositories_controller_spec.rb b/spec/requests/projects/harbor/repositories_controller_spec.rb
index cdb5a696d7e..751becaa20a 100644
--- a/spec/requests/projects/harbor/repositories_controller_spec.rb
+++ b/spec/requests/projects/harbor/repositories_controller_spec.rb
@@ -3,67 +3,8 @@
require 'spec_helper'
RSpec.describe Projects::Harbor::RepositoriesController do
- let_it_be(:project, reload: true) { create(:project) }
- let_it_be(:user) { create(:user) }
-
- shared_examples 'responds with 404 status' do
- it 'returns 404' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- shared_examples 'responds with 200 status' do
- it 'renders the index template' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:index)
- end
- end
-
- before do
- stub_feature_flags(harbor_registry_integration: true)
- project.add_developer(user)
- sign_in(user)
- end
-
- describe 'GET #index' do
- subject do
- get project_harbor_registry_index_path(project)
- response
- end
-
- context 'with harbor registry feature flag enabled' do
- it_behaves_like 'responds with 200 status'
- end
-
- context 'with harbor registry feature flag disabled' do
- before do
- stub_feature_flags(harbor_registry_integration: false)
- end
-
- it_behaves_like 'responds with 404 status'
- end
- end
-
- describe 'GET #show' do
- subject do
- get project_harbor_registry_path(project, 1)
- response
- end
-
- context 'with harbor registry feature flag enabled' do
- it_behaves_like 'responds with 200 status'
- end
-
- context 'with harbor registry feature flag disabled' do
- before do
- stub_feature_flags(harbor_registry_integration: false)
- end
-
- it_behaves_like 'responds with 404 status'
- end
+ it_behaves_like 'a harbor repositories controller', anonymous_status_code: '302' do
+ let_it_be(:container, reload: true) { create(:project) }
+ let_it_be(:harbor_integration) { create(:harbor_integration, project: container) }
end
end
diff --git a/spec/requests/projects/harbor/tags_controller_spec.rb b/spec/requests/projects/harbor/tags_controller_spec.rb
new file mode 100644
index 00000000000..119d1c746ac
--- /dev/null
+++ b/spec/requests/projects/harbor/tags_controller_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Harbor::TagsController do
+ it_behaves_like 'a harbor tags controller', anonymous_status_code: '302' do
+ let_it_be(:container) { create(:project) }
+ let_it_be(:harbor_integration) { create(:harbor_integration, project: container) }
+ end
+end
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index 5c2ef62683e..9f5f821cc61 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -59,6 +59,18 @@ RSpec.shared_examples 'groups routing' do
expect(get('/groups/gitlabhq/-/boards')).to route_to('groups/boards#index', group_id: 'gitlabhq')
end
+
+ it 'routes to the harbor repositories controller' do
+ expect(get("groups/#{group_path}/-/harbor/repositories")).to route_to('groups/harbor/repositories#index', group_id: group_path)
+ end
+
+ it 'routes to the harbor artifacts controller' do
+ expect(get("groups/#{group_path}/-/harbor/repositories/test/artifacts")).to route_to('groups/harbor/artifacts#index', group_id: group_path, repository_id: 'test')
+ end
+
+ it 'routes to the harbor tags controller' do
+ expect(get("groups/#{group_path}/-/harbor/repositories/test/artifacts/test/tags")).to route_to('groups/harbor/tags#index', group_id: group_path, repository_id: 'test', artifact_id: 'test')
+ end
end
RSpec.describe "Groups", "routing" do
diff --git a/spec/serializers/integrations/harbor_serializers/artifact_entity_spec.rb b/spec/serializers/integrations/harbor_serializers/artifact_entity_spec.rb
new file mode 100644
index 00000000000..c9a95c02e19
--- /dev/null
+++ b/spec/serializers/integrations/harbor_serializers/artifact_entity_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::HarborSerializers::ArtifactEntity do
+ let_it_be(:harbor_integration) { create(:harbor_integration) }
+
+ let(:artifact) do
+ {
+ "digest": "sha256:14d4f50961544fdb669075c442509f194bdc4c0e344bde06e35dbd55af842a38",
+ "id": 5,
+ "project_id": 14,
+ "push_time": "2022-03-22T09:04:56.170Z",
+ "repository_id": 5,
+ "size": 774790,
+ "tags": [
+ {
+ "artifact_id": 5,
+ "id": 7,
+ "immutable": false,
+ "name": "2",
+ "push_time": "2022-03-22T09:05:04.844Z",
+ "repository_id": 5,
+ "signed": false
+ },
+ {
+ "artifact_id": 5,
+ "id": 6,
+ "immutable": false,
+ "name": "1",
+ "push_time": "2022-03-22T09:04:56.186Z",
+ "repository_id": 5,
+ "signed": false
+ }
+ ],
+ "type": "IMAGE"
+ }.deep_stringify_keys
+ end
+
+ subject { described_class.new(artifact).as_json }
+
+ it 'returns the Harbor artifact' do
+ expect(subject).to include({
+ harbor_id: 5,
+ size: 774790,
+ push_time: "2022-03-22T09:04:56.170Z".to_datetime,
+ digest: "sha256:14d4f50961544fdb669075c442509f194bdc4c0e344bde06e35dbd55af842a38",
+ tags: %w[2 1]
+ })
+ end
+end
diff --git a/spec/serializers/integrations/harbor_serializers/artifact_serializer_spec.rb b/spec/serializers/integrations/harbor_serializers/artifact_serializer_spec.rb
new file mode 100644
index 00000000000..9879c0a6434
--- /dev/null
+++ b/spec/serializers/integrations/harbor_serializers/artifact_serializer_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::HarborSerializers::ArtifactSerializer do
+ it 'represents Integrations::HarborSerializers::ArtifactEntity entities' do
+ expect(described_class.entity_class).to eq(Integrations::HarborSerializers::ArtifactEntity)
+ end
+end
diff --git a/spec/serializers/integrations/harbor_serializers/repository_entity_spec.rb b/spec/serializers/integrations/harbor_serializers/repository_entity_spec.rb
new file mode 100644
index 00000000000..29708bd0416
--- /dev/null
+++ b/spec/serializers/integrations/harbor_serializers/repository_entity_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::HarborSerializers::RepositoryEntity do
+ let_it_be(:harbor_integration) { create(:harbor_integration) }
+
+ let(:repo) do
+ {
+ "artifact_count" => 1,
+ "creation_time" => "2022-03-13T09:36:43.240Z",
+ "id" => 1,
+ "name" => "jihuprivate/busybox",
+ "project_id" => 4,
+ "pull_count" => 0,
+ "update_time" => "2022-03-13T09:36:43.240Z"
+ }.deep_stringify_keys
+ end
+
+ subject { described_class.new(repo, url: "https://demo.goharbor.io", project_name: "jihuprivate").as_json }
+
+ context 'with normal repository data' do
+ it 'returns the Harbor repository' do
+ expect(subject).to include({
+ artifact_count: 1,
+ creation_time: "2022-03-13T09:36:43.240Z".to_datetime,
+ harbor_id: 1,
+ name: "jihuprivate/busybox",
+ harbor_project_id: 4,
+ pull_count: 0,
+ update_time: "2022-03-13T09:36:43.240Z".to_datetime,
+ location: "https://demo.goharbor.io/harbor/projects/4/repositories/busybox"
+ })
+ end
+ end
+
+ context 'with data that may contain path traversal attacks' do
+ before do
+ repo["project_id"] = './../../../../../etc/hosts'
+ end
+
+ it 'returns empty location' do
+ expect(subject).to include({
+ artifact_count: 1,
+ creation_time: "2022-03-13T09:36:43.240Z".to_datetime,
+ harbor_id: 1,
+ name: "jihuprivate/busybox",
+ harbor_project_id: './../../../../../etc/hosts',
+ pull_count: 0,
+ update_time: "2022-03-13T09:36:43.240Z".to_datetime,
+ location: "https://demo.goharbor.io/"
+ })
+ end
+ end
+end
diff --git a/spec/serializers/integrations/harbor_serializers/repository_serializer_spec.rb b/spec/serializers/integrations/harbor_serializers/repository_serializer_spec.rb
new file mode 100644
index 00000000000..1a4235bea1e
--- /dev/null
+++ b/spec/serializers/integrations/harbor_serializers/repository_serializer_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::HarborSerializers::RepositorySerializer do
+ it 'represents Integrations::HarborSerializers::RepositoryEntity entities' do
+ expect(described_class.entity_class).to eq(Integrations::HarborSerializers::RepositoryEntity)
+ end
+end
diff --git a/spec/serializers/integrations/harbor_serializers/tag_entity_spec.rb b/spec/serializers/integrations/harbor_serializers/tag_entity_spec.rb
new file mode 100644
index 00000000000..f4bc5f71d5b
--- /dev/null
+++ b/spec/serializers/integrations/harbor_serializers/tag_entity_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::HarborSerializers::TagEntity do
+ let_it_be(:harbor_integration) { create(:harbor_integration) }
+
+ let(:push_time) { "2022-03-22T09:04:56.186Z" }
+ let(:pull_time) { "2022-03-23T09:04:56.186Z" }
+
+ let(:tag) do
+ {
+ "artifact_id": 5,
+ "id": 6,
+ "immutable": false,
+ "name": "1",
+ "push_time": push_time,
+ "pull_time": pull_time,
+ "repository_id": 5,
+ "signed": false
+ }.deep_stringify_keys
+ end
+
+ subject { described_class.new(tag).as_json }
+
+ it 'returns the Harbor artifact' do
+ expect(subject).to include({
+ harbor_repository_id: 5,
+ harbor_artifact_id: 5,
+ harbor_id: 6,
+ name: "1",
+ pull_time: pull_time.to_datetime.utc,
+ push_time: push_time.to_datetime.utc,
+ signed: false,
+ immutable: false
+ })
+ end
+end
diff --git a/spec/serializers/integrations/harbor_serializers/tag_serializer_spec.rb b/spec/serializers/integrations/harbor_serializers/tag_serializer_spec.rb
new file mode 100644
index 00000000000..45fee0b9b17
--- /dev/null
+++ b/spec/serializers/integrations/harbor_serializers/tag_serializer_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::HarborSerializers::TagSerializer do
+ it 'represents Integrations::HarborSerializers::TagEntity entities' do
+ expect(described_class.entity_class).to eq(Integrations::HarborSerializers::TagEntity)
+ end
+end
diff --git a/spec/support/helpers/harbor_helper.rb b/spec/support/helpers/harbor_helper.rb
new file mode 100644
index 00000000000..3f13710ede6
--- /dev/null
+++ b/spec/support/helpers/harbor_helper.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module HarborHelper
+ def harbor_repository_url(container, *args)
+ if container.is_a?(Project)
+ project_harbor_repositories_path(container, *args)
+ else
+ group_harbor_repositories_path(container, *args)
+ end
+ end
+
+ def harbor_artifact_url(container, *args)
+ if container.is_a?(Project)
+ project_harbor_repository_artifacts_path(container, *args)
+ else
+ group_harbor_repository_artifacts_path(container, *args)
+ end
+ end
+
+ def harbor_tag_url(container, *args)
+ if container.is_a?(Project)
+ project_harbor_repository_artifact_tags_path(container, *args)
+ else
+ group_harbor_repository_artifact_tags_path(container, *args)
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index 483bca07ba6..eec6e92c5fe 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -31,6 +31,7 @@ RSpec.shared_context 'GroupPolicy context' do
admin_milestone
admin_issue_board
read_container_image
+ read_harbor_registry
read_metrics_dashboard_annotation
read_prometheus
read_crm_contact
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 3bb05d0b6a6..789b385c435 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -30,7 +30,7 @@ RSpec.shared_context 'ProjectPolicy context' do
create_snippet create_incident daily_statistics create_merge_request_in download_code
download_wiki_code fork_project metrics_dashboard read_build
read_commit_status read_confidential_issues read_container_image
- read_deployment read_environment read_merge_request
+ read_harbor_registry read_deployment read_environment read_merge_request
read_metrics_dashboard_annotation read_pipeline read_prometheus
read_sentry_issue update_issue create_merge_request_in
]
diff --git a/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb b/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb
new file mode 100644
index 00000000000..85fcd426e3d
--- /dev/null
+++ b/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a harbor artifacts controller' do |args|
+ include HarborHelper
+ let_it_be(:user) { create(:user) }
+ let_it_be(:unauthorized_user) { create(:user) }
+ let_it_be(:json_header) { { accept: 'application/json' } }
+
+ let(:mock_artifacts) do
+ [
+ {
+ "digest": "sha256:661e8e44e5d7290fbd42d0495ab4ff6fdf1ad251a9f358969b3264a22107c14d",
+ "icon": "sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06",
+ "id": 1,
+ "project_id": 1,
+ "pull_time": "0001-01-01T00:00:00.000Z",
+ "push_time": "2022-04-23T08:04:08.901Z",
+ "repository_id": 1,
+ "size": 126745886,
+ "tags": [
+ {
+ "artifact_id": 1,
+ "id": 1,
+ "immutable": false,
+ "name": "2",
+ "pull_time": "0001-01-01T00:00:00.000Z",
+ "push_time": "2022-04-23T08:04:08.920Z",
+ "repository_id": 1,
+ "signed": false
+ }
+ ],
+ "type": "IMAGE"
+ }
+ ]
+ end
+
+ let(:repository_id) { 'test' }
+
+ shared_examples 'responds with 404 status' do
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples 'responds with 200 status with json' do
+ it 'renders the index template' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).not_to render_template(:index)
+ end
+ end
+
+ shared_examples 'responds with 302 status' do
+ it 'returns 302' do
+ subject
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ shared_examples 'responds with 422 status with json' do
+ it 'returns 422' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+
+ before do
+ stub_request(:get,
+ "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts"\
+ "?page=1&page_size=10&with_tag=true")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ }).to_return(status: 200, body: mock_artifacts.to_json, headers: { "x-total-count": 2 })
+ container.add_reporter(user)
+ sign_in(user)
+ end
+
+ describe 'GET #index.json' do
+ subject do
+ get harbor_artifact_url(container, repository_id), headers: json_header
+ end
+
+ context 'with harbor registry feature flag enabled' do
+ it_behaves_like 'responds with 200 status with json'
+ end
+
+ context 'with harbor registry feature flag disabled' do
+ before do
+ stub_feature_flags(harbor_registry_integration: false)
+ end
+
+ it_behaves_like 'responds with 404 status'
+ end
+
+ context 'with anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it_behaves_like "responds with #{args[:anonymous_status_code]} status"
+ end
+
+ context 'with unauthorized user' do
+ before do
+ sign_in(unauthorized_user)
+ end
+
+ it_behaves_like 'responds with 404 status'
+ end
+
+ context 'with valid params' do
+ context 'with valid repository' do
+ subject do
+ get harbor_artifact_url(container, repository_id), headers: json_header
+ end
+
+ it_behaves_like 'responds with 200 status with json'
+ end
+
+ context 'with valid page' do
+ subject do
+ get harbor_artifact_url(container, repository_id, page: '1'), headers: json_header
+ end
+
+ it_behaves_like 'responds with 200 status with json'
+ end
+
+ context 'with valid limit' do
+ subject do
+ get harbor_artifact_url(container, repository_id, limit: '10'), headers: json_header
+ end
+
+ it_behaves_like 'responds with 200 status with json'
+ end
+ end
+
+ context 'with invalid params' do
+ context 'with invalid page' do
+ subject do
+ get harbor_artifact_url(container, repository_id, page: 'aaa'), headers: json_header
+ end
+
+ it_behaves_like 'responds with 422 status with json'
+ end
+
+ context 'with invalid limit' do
+ subject do
+ get harbor_artifact_url(container, repository_id, limit: 'aaa'), headers: json_header
+ end
+
+ it_behaves_like 'responds with 422 status with json'
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/harbor/container_shared_examples.rb b/spec/support/shared_examples/harbor/container_shared_examples.rb
new file mode 100644
index 00000000000..57274e0b457
--- /dev/null
+++ b/spec/support/shared_examples/harbor/container_shared_examples.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'raises NotImplementedError when calling #container' do
+ describe '#container' do
+ it 'raises NotImplementedError' do
+ expect { controller.send(:container) }.to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb b/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb
new file mode 100644
index 00000000000..b35595a10b2
--- /dev/null
+++ b/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb
@@ -0,0 +1,172 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a harbor repositories controller' do |args|
+ include HarborHelper
+ let_it_be(:user) { create(:user) }
+ let_it_be(:unauthorized_user) { create(:user) }
+ let_it_be(:json_header) { { accept: 'application/json' } }
+
+ let(:mock_repositories) do
+ [
+ {
+ "artifact_count": 6,
+ "creation_time": "2022-04-24T10:59:02.719Z",
+ "id": 33,
+ "name": "test/photon",
+ "project_id": 3,
+ "pull_count": 12,
+ "update_time": "2022-04-24T11:06:27.678Z"
+ },
+ {
+ "artifact_count": 1,
+ "creation_time": "2022-04-23T08:04:08.880Z",
+ "id": 1,
+ "name": "test/gemnasium",
+ "project_id": 3,
+ "pull_count": 0,
+ "update_time": "2022-04-23T08:04:08.880Z"
+ }
+ ]
+ end
+
+ shared_examples 'responds with 404 status' do
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples 'responds with 200 status with html' do
+ it 'renders the index template' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index)
+ end
+ end
+
+ shared_examples 'responds with 302 status' do
+ it 'returns 302' do
+ subject
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ shared_examples 'responds with 200 status with json' do
+ it 'renders the index template' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).not_to render_template(:index)
+ end
+ end
+
+ shared_examples 'responds with 422 status with json' do
+ it 'returns 422' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+
+ before do
+ stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories?page=1&page_size=10")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ }).to_return(status: 200, body: mock_repositories.to_json, headers: { "x-total-count": 2 })
+ container.add_reporter(user)
+ sign_in(user)
+ end
+
+ describe 'GET #index.html' do
+ subject do
+ get harbor_repository_url(container)
+ end
+
+ context 'with harbor registry feature flag enabled' do
+ it_behaves_like 'responds with 200 status with html'
+ end
+
+ context 'with harbor registry feature flag disabled' do
+ before do
+ stub_feature_flags(harbor_registry_integration: false)
+ end
+
+ it_behaves_like 'responds with 404 status'
+ end
+
+ context 'with anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it_behaves_like "responds with #{args[:anonymous_status_code]} status"
+ end
+
+ context 'with unauthorized user' do
+ before do
+ sign_in(unauthorized_user)
+ end
+
+ it_behaves_like 'responds with 404 status'
+ end
+ end
+
+ describe 'GET #index.json' do
+ subject do
+ get harbor_repository_url(container), headers: json_header
+ end
+
+ context 'with harbor registry feature flag enabled' do
+ it_behaves_like 'responds with 200 status with json'
+ end
+
+ context 'with harbor registry feature flag disabled' do
+ before do
+ stub_feature_flags(harbor_registry_integration: false)
+ end
+
+ it_behaves_like 'responds with 404 status'
+ end
+
+ context 'with valid params' do
+ context 'with valid page params' do
+ subject do
+ get harbor_repository_url(container, page: '1'), headers: json_header
+ end
+
+ it_behaves_like 'responds with 200 status with json'
+ end
+
+ context 'with valid limit params' do
+ subject do
+ get harbor_repository_url(container, limit: '10'), headers: json_header
+ end
+
+ it_behaves_like 'responds with 200 status with json'
+ end
+ end
+
+ context 'with invalid params' do
+ context 'with invalid page params' do
+ subject do
+ get harbor_repository_url(container, page: 'aaa'), headers: json_header
+ end
+
+ it_behaves_like 'responds with 422 status with json'
+ end
+
+ context 'with invalid limit params' do
+ subject do
+ get harbor_repository_url(container, limit: 'aaa'), headers: json_header
+ end
+
+ it_behaves_like 'responds with 422 status with json'
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb b/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb
new file mode 100644
index 00000000000..46fea7fdff6
--- /dev/null
+++ b/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a harbor tags controller' do |args|
+ include HarborHelper
+ let_it_be(:user) { create(:user) }
+ let_it_be(:unauthorized_user) { create(:user) }
+ let_it_be(:json_header) { { accept: 'application/json' } }
+
+ let(:mock_artifacts) do
+ [
+ {
+ "artifact_id": 1,
+ "id": 1,
+ "immutable": false,
+ "name": "2",
+ "pull_time": "0001-01-01T00:00:00.000Z",
+ "push_time": "2022-04-23T08:04:08.920Z",
+ "repository_id": 1,
+ "signed": false
+ }
+ ]
+ end
+
+ let(:repository_id) { 'test' }
+ let(:artifact_id) { '1' }
+
+ shared_examples 'responds with 404 status' do
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples 'responds with 200 status with json' do
+ it 'renders the index template' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).not_to render_template(:index)
+ end
+ end
+
+ shared_examples 'responds with 302 status' do
+ it 'returns 302' do
+ subject
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ shared_examples 'responds with 422 status with json' do
+ it 'returns 422' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+
+ before do
+ stub_request(:get,
+ "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts/1/tags"\
+ "?page=1&page_size=10")
+ .with(
+ headers: {
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
+ 'Content-Type': 'application/json'
+ }).to_return(status: 200, body: mock_artifacts.to_json, headers: { "x-total-count": 2 })
+ container.add_reporter(user)
+ sign_in(user)
+ end
+
+ describe 'GET #index.json' do
+ subject do
+ get(harbor_tag_url(container, repository_id, artifact_id),
+ headers: json_header)
+ end
+
+ context 'with harbor registry feature flag enabled' do
+ it_behaves_like 'responds with 200 status with json'
+ end
+
+ context 'with harbor registry feature flag disabled' do
+ before do
+ stub_feature_flags(harbor_registry_integration: false)
+ end
+
+ it_behaves_like 'responds with 404 status'
+ end
+
+ context 'with anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it_behaves_like "responds with #{args[:anonymous_status_code]} status"
+ end
+
+ context 'with unauthorized user' do
+ before do
+ sign_in(unauthorized_user)
+ end
+
+ it_behaves_like 'responds with 404 status'
+ end
+
+ context 'with valid params' do
+ context 'with valid repository' do
+ subject do
+ get harbor_tag_url(container, repository_id, artifact_id), headers: json_header
+ end
+
+ it_behaves_like 'responds with 200 status with json'
+ end
+
+ context 'with valid page' do
+ subject do
+ get(harbor_tag_url(container, repository_id, artifact_id, page: '1'),
+ headers: json_header)
+ end
+
+ it_behaves_like 'responds with 200 status with json'
+ end
+
+ context 'with valid limit' do
+ subject do
+ get(harbor_tag_url(container, repository_id, artifact_id, limit: '10'),
+ headers: json_header)
+ end
+
+ it_behaves_like 'responds with 200 status with json'
+ end
+ end
+
+ context 'with invalid params' do
+ context 'with invalid page' do
+ subject do
+ get(harbor_tag_url(container, repository_id, artifact_id, page: 'aaa'),
+ headers: json_header)
+ end
+
+ it_behaves_like 'responds with 422 status with json'
+ end
+
+ context 'with invalid limit' do
+ subject do
+ get(harbor_tag_url(container, repository_id, artifact_id, limit: 'aaa'),
+ headers: json_header)
+ end
+
+ it_behaves_like 'responds with 422 status with json'
+ end
+ end
+ end
+end