summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue7
-rw-r--r--app/models/ci/job_artifact.rb7
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/wiki_page.rb4
-rw-r--r--app/models/wiki_page/meta.rb108
-rw-r--r--app/services/event_create_service.rb28
-rw-r--r--app/services/git/wiki_push_service.rb57
-rw-r--r--app/services/git/wiki_push_service/change.rb67
-rw-r--r--app/services/wiki_pages/base_service.rb7
-rw-r--r--app/services/wiki_pages/event_create_service.rb30
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml5
-rw-r--r--changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml5
-rw-r--r--changelogs/unreleased/cluster-applications-artifact.yml5
-rw-r--r--changelogs/unreleased/remove_admin_settings_geo_navigation.yml5
-rw-r--r--doc/user/clusters/applications.md6
-rw-r--r--doc/user/clusters/crossplane.md6
-rw-r--r--doc/user/clusters/environments.md6
-rw-r--r--doc/user/clusters/management_project.md6
-rw-r--r--doc/user/group/clusters/index.md3
-rw-r--r--doc/user/infrastructure/index.md6
-rw-r--r--doc/user/project/clusters/add_eks_clusters.md6
-rw-r--r--doc/user/project/clusters/add_gke_clusters.md6
-rw-r--r--doc/user/project/clusters/add_remove_clusters.md6
-rw-r--r--doc/user/project/clusters/kubernetes_pod_logs.md6
-rw-r--r--doc/user/project/clusters/runbooks/index.md6
-rw-r--r--doc/user/project/clusters/serverless/aws.md6
-rw-r--r--doc/user/project/clusters/serverless/index.md6
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb3
-rw-r--r--lib/gitlab/kubernetes/network_policy.rb91
-rw-r--r--spec/factories/design_management/versions.rb2
-rw-r--r--spec/factories/git_wiki_commit_details.rb15
-rw-r--r--spec/factories/sequences.rb1
-rw-r--r--spec/factories/wiki_pages.rb1
-rw-r--r--spec/frontend/sidebar/confidential/edit_form_buttons_spec.js41
-rw-r--r--spec/frontend/sidebar/confidential/edit_form_spec.js45
-rw-r--r--spec/frontend/sidebar/confidential_edit_buttons_spec.js35
-rw-r--r--spec/frontend/sidebar/confidential_edit_form_buttons_spec.js35
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb1
-rw-r--r--spec/lib/gitlab/kubernetes/network_policy_spec.rb224
-rw-r--r--spec/models/event_spec.rb23
-rw-r--r--spec/models/wiki_page/meta_spec.rb87
-rw-r--r--spec/models/wiki_page_spec.rb14
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/event_create_service_spec.rb13
-rw-r--r--spec/services/git/wiki_push_service/change_spec.rb109
-rw-r--r--spec/services/git/wiki_push_service_spec.rb338
-rw-r--r--spec/services/wiki_pages/event_create_service_spec.rb87
-rw-r--r--spec/workers/post_receive_spec.rb31
48 files changed, 1454 insertions, 156 deletions
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 5d0e39e8195..e106afea9f5 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -40,7 +40,12 @@ export default {
<button type="button" class="btn btn-default append-right-10" @click="closeForm">
{{ __('Cancel') }}
</button>
- <button type="button" class="btn btn-close" @click.prevent="submitForm">
+ <button
+ type="button"
+ class="btn btn-close"
+ data-testid="confidential-toggle"
+ @click.prevent="submitForm"
+ >
{{ toggleButtonText }}
</button>
</div>
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 3e06fa0957c..5810322b088 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -35,7 +35,8 @@ module Ci
lsif: 'lsif.json',
dotenv: '.env',
cobertura: 'cobertura-coverage.xml',
- terraform: 'tfplan.json'
+ terraform: 'tfplan.json',
+ cluster_applications: 'gl-cluster-applications.json'
}.freeze
INTERNAL_TYPES = {
@@ -52,6 +53,7 @@ module Ci
lsif: :gzip,
dotenv: :gzip,
cobertura: :gzip,
+ cluster_applications: :gzip,
# All these file formats use `raw` as we need to store them uncompressed
# for Frontend to fetch the files and do analysis
@@ -153,7 +155,8 @@ module Ci
dotenv: 16,
cobertura: 17,
terraform: 18, # Transformed json
- accessibility: 19
+ accessibility: 19,
+ cluster_applications: 20
}
enum file_format: {
diff --git a/app/models/event.rb b/app/models/event.rb
index 48f745649e4..12b85697690 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -96,6 +96,8 @@ class Event < ApplicationRecord
end
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
+ scope :for_wiki_meta, ->(meta) { where(target_type: 'WikiPage::Meta', target_id: meta.id) }
+ scope :created_at, ->(time) { where(created_at: time) }
# Authors are required as they're used to display who pushed data.
#
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index b3fc4941b12..319cdd38d93 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -295,6 +295,10 @@ class WikiPage
'wiki_page'
end
+ def version_commit_timestamp
+ version&.commit&.committed_date
+ end
+
private
def serialize_front_matter(hash)
diff --git a/app/models/wiki_page/meta.rb b/app/models/wiki_page/meta.rb
index 2af7d86ebcc..474968122b1 100644
--- a/app/models/wiki_page/meta.rb
+++ b/app/models/wiki_page/meta.rb
@@ -5,6 +5,7 @@ class WikiPage
include Gitlab::Utils::StrongMemoize
CanonicalSlugConflictError = Class.new(ActiveRecord::RecordInvalid)
+ WikiPageInvalid = Class.new(ArgumentError)
self.table_name = 'wiki_page_meta'
@@ -23,46 +24,62 @@ class WikiPage
alias_method :resource_parent, :project
- # Return the (updated) WikiPage::Meta record for a given wiki page
- #
- # If none is found, then a new record is created, and its fields are set
- # to reflect the wiki_page passed.
- #
- # @param [String] last_known_slug
- # @param [WikiPage] wiki_page
- #
- # As with all `find_or_create` methods, this one raises errors on
- # validation issues.
- def self.find_or_create(last_known_slug, wiki_page)
- project = wiki_page.wiki.project
- known_slugs = [last_known_slug, wiki_page.slug].compact.uniq
- raise 'no slugs!' if known_slugs.empty?
-
- transaction do
- found = find_by_canonical_slug(known_slugs, project)
- meta = found || create(title: wiki_page.title, project_id: project.id)
-
- meta.update_state(found.nil?, known_slugs, wiki_page)
-
- # We don't need to run validations here, since find_by_canonical_slug
- # guarantees that there is no conflict in canonical_slug, and DB
- # constraints on title and project_id enforce our other invariants
- # This saves us a query.
- meta
+ class << self
+ # Return the (updated) WikiPage::Meta record for a given wiki page
+ #
+ # If none is found, then a new record is created, and its fields are set
+ # to reflect the wiki_page passed.
+ #
+ # @param [String] last_known_slug
+ # @param [WikiPage] wiki_page
+ #
+ # This method raises errors on validation issues.
+ def find_or_create(last_known_slug, wiki_page)
+ raise WikiPageInvalid unless wiki_page.valid?
+
+ project = wiki_page.wiki.project
+ known_slugs = [last_known_slug, wiki_page.slug].compact.uniq
+ raise 'No slugs found! This should not be possible.' if known_slugs.empty?
+
+ transaction do
+ updates = wiki_page_updates(wiki_page)
+ found = find_by_canonical_slug(known_slugs, project)
+ meta = found || create!(updates.merge(project_id: project.id))
+
+ meta.update_state(found.nil?, known_slugs, wiki_page, updates)
+
+ # We don't need to run validations here, since find_by_canonical_slug
+ # guarantees that there is no conflict in canonical_slug, and DB
+ # constraints on title and project_id enforce our other invariants
+ # This saves us a query.
+ meta
+ end
end
- end
- def self.find_by_canonical_slug(canonical_slug, project)
- meta, conflict = with_canonical_slug(canonical_slug)
- .where(project_id: project.id)
- .limit(2)
+ def find_by_canonical_slug(canonical_slug, project)
+ meta, conflict = with_canonical_slug(canonical_slug)
+ .where(project_id: project.id)
+ .limit(2)
- if conflict.present?
- meta.errors.add(:canonical_slug, 'Duplicate value found')
- raise CanonicalSlugConflictError.new(meta)
+ if conflict.present?
+ meta.errors.add(:canonical_slug, 'Duplicate value found')
+ raise CanonicalSlugConflictError.new(meta)
+ end
+
+ meta
end
- meta
+ private
+
+ def wiki_page_updates(wiki_page)
+ last_commit_date = wiki_page.version_commit_timestamp || Time.now.utc
+
+ {
+ title: wiki_page.title,
+ created_at: last_commit_date,
+ updated_at: last_commit_date
+ }
+ end
end
def canonical_slug
@@ -85,24 +102,21 @@ class WikiPage
@canonical_slug = slug
end
- def update_state(created, known_slugs, wiki_page)
- update_wiki_page_attributes(wiki_page)
+ def update_state(created, known_slugs, wiki_page, updates)
+ update_wiki_page_attributes(updates)
insert_slugs(known_slugs, created, wiki_page.slug)
self.canonical_slug = wiki_page.slug
end
- def update_columns(attrs = {})
- super(attrs.reverse_merge(updated_at: Time.now.utc))
- end
-
- def self.update_all(attrs = {})
- super(attrs.reverse_merge(updated_at: Time.now.utc))
- end
-
private
- def update_wiki_page_attributes(page)
- update_columns(title: page.title) unless page.title == title
+ def update_wiki_page_attributes(updates)
+ # Remove all unnecessary updates:
+ updates.delete(:updated_at) if updated_at == updates[:updated_at]
+ updates.delete(:created_at) if created_at <= updates[:created_at]
+ updates.delete(:title) if title == updates[:title]
+
+ update_columns(updates) unless updates.empty?
end
def insert_slugs(strings, is_new, canonical_slug)
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 0b044e1679a..522f36cda46 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -85,18 +85,40 @@ class EventCreateService
# Create a new wiki page event
#
# @param [WikiPage::Meta] wiki_page_meta The event target
- # @param [User] current_user The event author
+ # @param [User] author The event author
# @param [Integer] action One of the Event::WIKI_ACTIONS
- def wiki_event(wiki_page_meta, current_user, action)
+ #
+ # @return a tuple of event and either :found or :created
+ def wiki_event(wiki_page_meta, author, action)
return unless Feature.enabled?(:wiki_events)
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
- create_record_event(wiki_page_meta, current_user, action)
+ if duplicate = existing_wiki_event(wiki_page_meta, action)
+ return duplicate
+ end
+
+ event = create_record_event(wiki_page_meta, author, action)
+ # Ensure that the event is linked in time to the metadata, for non-deletes
+ unless action == Event::DESTROYED
+ time_stamp = wiki_page_meta.updated_at
+ event.update_columns(updated_at: time_stamp, created_at: time_stamp)
+ end
+
+ event
end
private
+ def existing_wiki_event(wiki_page_meta, action)
+ if action == Event::DESTROYED
+ most_recent = Event.for_wiki_meta(wiki_page_meta).recent.first
+ return most_recent if most_recent.present? && most_recent.action == action
+ else
+ Event.for_wiki_meta(wiki_page_meta).created_at(wiki_page_meta.updated_at).first
+ end
+ end
+
def create_record_event(record, current_user, status)
create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name)
end
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index d4267d4a3c5..8bdbc28f3e8 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -2,8 +2,63 @@
module Git
class WikiPushService < ::BaseService
+ # Maximum number of change events we will process on any single push
+ MAX_CHANGES = 100
+
def execute
- # This is used in EE
+ process_changes
+ end
+
+ private
+
+ def process_changes
+ return unless can_process_wiki_events?
+
+ push_changes.take(MAX_CHANGES).each do |change| # rubocop:disable CodeReuse/ActiveRecord
+ next unless change.page.present?
+
+ response = create_event_for(change)
+ log_error(response.message) if response.error?
+ end
+ end
+
+ def can_process_wiki_events?
+ Feature.enabled?(:wiki_events) && Feature.enabled?(:wiki_events_on_git_push, project)
+ end
+
+ def push_changes
+ default_branch_changes.flat_map do |change|
+ raw_changes(change).map { |raw| Git::WikiPushService::Change.new(wiki, change, raw) }
+ end
+ end
+
+ def raw_changes(change)
+ wiki.repository.raw.raw_changes_between(change[:oldrev], change[:newrev])
+ end
+
+ def wiki
+ project.wiki
+ end
+
+ def create_event_for(change)
+ event_service.execute(change.last_known_slug, change.page, change.event_action)
+ end
+
+ def event_service
+ @event_service ||= WikiPages::EventCreateService.new(current_user)
+ end
+
+ def on_default_branch?(change)
+ project.wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref])
+ end
+
+ # See: [Gitlab::GitPostReceive#changes]
+ def changes
+ params[:changes] || []
+ end
+
+ def default_branch_changes
+ @default_branch_changes ||= changes.select { |change| on_default_branch?(change) }
end
end
end
diff --git a/app/services/git/wiki_push_service/change.rb b/app/services/git/wiki_push_service/change.rb
new file mode 100644
index 00000000000..8685850165a
--- /dev/null
+++ b/app/services/git/wiki_push_service/change.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Git
+ class WikiPushService
+ class Change
+ include Gitlab::Utils::StrongMemoize
+
+ # @param [ProjectWiki] wiki
+ # @param [Hash] change - must have keys `:oldrev` and `:newrev`
+ # @param [Gitlab::Git::RawDiffChange] raw_change
+ def initialize(project_wiki, change, raw_change)
+ @wiki, @raw_change, @change = project_wiki, raw_change, change
+ end
+
+ def page
+ strong_memoize(:page) { wiki.find_page(slug, revision) }
+ end
+
+ # See [Gitlab::Git::RawDiffChange#extract_operation] for the
+ # definition of the full range of operation values.
+ def event_action
+ case raw_change.operation
+ when :added
+ Event::CREATED
+ when :deleted
+ Event::DESTROYED
+ else
+ Event::UPDATED
+ end
+ end
+
+ def last_known_slug
+ strip_extension(raw_change.old_path || raw_change.new_path)
+ end
+
+ private
+
+ attr_reader :raw_change, :change, :wiki
+
+ def filename
+ return raw_change.old_path if deleted?
+
+ raw_change.new_path
+ end
+
+ def slug
+ strip_extension(filename)
+ end
+
+ def revision
+ return change[:oldrev] if deleted?
+
+ change[:newrev]
+ end
+
+ def deleted?
+ raw_change.operation == :deleted
+ end
+
+ def strip_extension(filename)
+ return unless filename
+
+ File.basename(filename, File.extname(filename))
+ end
+ end
+ end
+end
diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb
index 2e774973ca5..2a2cbd7f7be 100644
--- a/app/services/wiki_pages/base_service.rb
+++ b/app/services/wiki_pages/base_service.rb
@@ -46,12 +46,9 @@ module WikiPages
def create_wiki_event(page)
return unless ::Feature.enabled?(:wiki_events)
- slug = slug_for_page(page)
+ response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action)
- Event.transaction do
- wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
- EventCreateService.new.wiki_event(wiki_page_meta, current_user, event_action)
- end
+ log_error(response.message) if response.error?
end
def slug_for_page(page)
diff --git a/app/services/wiki_pages/event_create_service.rb b/app/services/wiki_pages/event_create_service.rb
new file mode 100644
index 00000000000..18a45d057a9
--- /dev/null
+++ b/app/services/wiki_pages/event_create_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module WikiPages
+ class EventCreateService
+ # @param [User] author The event author
+ def initialize(author)
+ raise ArgumentError, 'author must not be nil' unless author
+
+ @author = author
+ end
+
+ def execute(slug, page, action)
+ return ServiceResponse.success(message: 'No event created as `wiki_events` feature is disabled') unless ::Feature.enabled?(:wiki_events)
+
+ event = Event.transaction do
+ wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
+
+ ::EventCreateService.new.wiki_event(wiki_page_meta, author, action)
+ end
+
+ ServiceResponse.success(payload: { event: event })
+ rescue ::EventCreateService::IllegalActionError, ::ActiveRecord::ActiveRecordError => e
+ ServiceResponse.error(message: e.message, payload: { error: e })
+ end
+
+ private
+
+ attr_reader :author
+ end
+end
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 52964dd6739..3151368bb3f 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -271,11 +271,6 @@
= link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_item' } do
%span
= _('Network')
- - if template_exists?('admin/geo/settings/show')
- = nav_link do
- = link_to geo_admin_application_settings_path, title: _('Geo') do
- %span
- = _('Geo')
= nav_link(path: 'application_settings#preferences') do
= link_to preferences_admin_application_settings_path, title: _('Preferences'), data: { qa_selector: 'admin_settings_preferences_link' } do
%span
diff --git a/changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml b/changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml
new file mode 100644
index 00000000000..95b5abbb55b
--- /dev/null
+++ b/changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml
@@ -0,0 +1,5 @@
+---
+title: Create Wiki activity events on pushes to Wiki git repository
+merge_request: 26624
+author:
+type: added
diff --git a/changelogs/unreleased/cluster-applications-artifact.yml b/changelogs/unreleased/cluster-applications-artifact.yml
new file mode 100644
index 00000000000..d027fcc11bc
--- /dev/null
+++ b/changelogs/unreleased/cluster-applications-artifact.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for cluster applications CI artifact report
+merge_request: 28866
+author:
+type: added
diff --git a/changelogs/unreleased/remove_admin_settings_geo_navigation.yml b/changelogs/unreleased/remove_admin_settings_geo_navigation.yml
new file mode 100644
index 00000000000..4635c8a7ca0
--- /dev/null
+++ b/changelogs/unreleased/remove_admin_settings_geo_navigation.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Admin -> Settings -> Geo navigation
+merge_request: 21005
+author: Lee Tickett
+type: other
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index be01540a293..697d62c9d79 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# GitLab Managed Apps
GitLab provides **GitLab Managed Apps**, a one-click install for various applications which can
diff --git a/doc/user/clusters/crossplane.md b/doc/user/clusters/crossplane.md
index 4e2ae87ecb9..9a1dde52956 100644
--- a/doc/user/clusters/crossplane.md
+++ b/doc/user/clusters/crossplane.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Crossplane configuration
Once Crossplane [is installed](applications.md#crossplane), it must be configured for
diff --git a/doc/user/clusters/environments.md b/doc/user/clusters/environments.md
index f83be85726a..ba96eef1e01 100644
--- a/doc/user/clusters/environments.md
+++ b/doc/user/clusters/environments.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Cluster Environments **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13392) for group-level clusters in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
diff --git a/doc/user/clusters/management_project.md b/doc/user/clusters/management_project.md
index 2b8ed83bdb2..03b4dc45015 100644
--- a/doc/user/clusters/management_project.md
+++ b/doc/user/clusters/management_project.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Cluster management project (alpha)
CAUTION: **Warning:**
diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md
index f15ad2165de..35fff638849 100644
--- a/doc/user/group/clusters/index.md
+++ b/doc/user/group/clusters/index.md
@@ -1,5 +1,8 @@
---
type: reference
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Group-level Kubernetes clusters
diff --git a/doc/user/infrastructure/index.md b/doc/user/infrastructure/index.md
index a1d09373e2c..e4ad6244bfe 100644
--- a/doc/user/infrastructure/index.md
+++ b/doc/user/infrastructure/index.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Infrastructure as code with GitLab managed Terraform State
[Terraform remote backends](https://www.terraform.io/docs/backends/index.html)
diff --git a/doc/user/project/clusters/add_eks_clusters.md b/doc/user/project/clusters/add_eks_clusters.md
index 7fa8ec6c5f3..712f8ea0adc 100644
--- a/doc/user/project/clusters/add_eks_clusters.md
+++ b/doc/user/project/clusters/add_eks_clusters.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Adding EKS clusters
GitLab supports adding new and existing EKS clusters.
diff --git a/doc/user/project/clusters/add_gke_clusters.md b/doc/user/project/clusters/add_gke_clusters.md
index 1195421f8fb..4094828323a 100644
--- a/doc/user/project/clusters/add_gke_clusters.md
+++ b/doc/user/project/clusters/add_gke_clusters.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Adding GKE clusters
GitLab supports adding new and existing GKE clusters.
diff --git a/doc/user/project/clusters/add_remove_clusters.md b/doc/user/project/clusters/add_remove_clusters.md
index dce273ce602..fddc9873f17 100644
--- a/doc/user/project/clusters/add_remove_clusters.md
+++ b/doc/user/project/clusters/add_remove_clusters.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Adding and removing Kubernetes clusters
GitLab offers integrated cluster creation for the following Kubernetes providers:
diff --git a/doc/user/project/clusters/kubernetes_pod_logs.md b/doc/user/project/clusters/kubernetes_pod_logs.md
index 1b7a6968e15..3d06a2cf0df 100644
--- a/doc/user/project/clusters/kubernetes_pod_logs.md
+++ b/doc/user/project/clusters/kubernetes_pod_logs.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Kubernetes Logs
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4752) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.0.
diff --git a/doc/user/project/clusters/runbooks/index.md b/doc/user/project/clusters/runbooks/index.md
index 279f08c2a72..dfed43470bc 100644
--- a/doc/user/project/clusters/runbooks/index.md
+++ b/doc/user/project/clusters/runbooks/index.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Runbooks
Runbooks are a collection of documented procedures that explain how to
diff --git a/doc/user/project/clusters/serverless/aws.md b/doc/user/project/clusters/serverless/aws.md
index 50cc0ef8d8d..124a0d4bf9f 100644
--- a/doc/user/project/clusters/serverless/aws.md
+++ b/doc/user/project/clusters/serverless/aws.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Deploying AWS Lambda function using GitLab CI/CD
GitLab allows users to easily deploy AWS Lambda functions and create rich serverless applications.
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index 418e16aa0c1..2156d96f92a 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Serverless
> Introduced in GitLab 11.5.
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 8bc6b96ac69..1a871e043a6 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -14,7 +14,7 @@ module Gitlab
ALLOWED_KEYS =
%i[junit codequality sast dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif
- dotenv cobertura terraform accessibility].freeze
+ dotenv cobertura terraform accessibility cluster_applications].freeze
attributes ALLOWED_KEYS
@@ -38,6 +38,7 @@ module Gitlab
validates :cobertura, array_of_strings_or_string: true
validates :terraform, array_of_strings_or_string: true
validates :accessibility, array_of_strings_or_string: true
+ validates :cluster_applications, array_of_strings_or_string: true
end
end
diff --git a/lib/gitlab/kubernetes/network_policy.rb b/lib/gitlab/kubernetes/network_policy.rb
new file mode 100644
index 00000000000..8a93cd100d7
--- /dev/null
+++ b/lib/gitlab/kubernetes/network_policy.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ class NetworkPolicy
+ def initialize(name:, namespace:, pod_selector:, ingress:, creation_timestamp: nil, policy_types: ["Ingress"], egress: nil)
+ @name = name
+ @namespace = namespace
+ @creation_timestamp = creation_timestamp
+ @pod_selector = pod_selector
+ @policy_types = policy_types
+ @ingress = ingress
+ @egress = egress
+ end
+
+ def self.from_yaml(manifest)
+ return unless manifest
+
+ policy = YAML.safe_load(manifest, symbolize_names: true)
+ return if !policy[:metadata] || !policy[:spec]
+
+ metadata = policy[:metadata]
+ spec = policy[:spec]
+ self.new(
+ name: metadata[:name],
+ namespace: metadata[:namespace],
+ pod_selector: spec[:podSelector],
+ policy_types: spec[:policyTypes],
+ ingress: spec[:ingress],
+ egress: spec[:egress]
+ )
+ rescue Psych::SyntaxError, Psych::DisallowedClass
+ nil
+ end
+
+ def self.from_resource(resource)
+ return unless resource
+ return if !resource[:metadata] || !resource[:spec]
+
+ metadata = resource[:metadata]
+ spec = resource[:spec].to_h
+ self.new(
+ name: metadata[:name],
+ namespace: metadata[:namespace],
+ creation_timestamp: metadata[:creationTimestamp],
+ pod_selector: spec[:podSelector],
+ policy_types: spec[:policyTypes],
+ ingress: spec[:ingress],
+ egress: spec[:egress]
+ )
+ end
+
+ def generate
+ ::Kubeclient::Resource.new.tap do |resource|
+ resource.metadata = metadata
+ resource.spec = spec
+ end
+ end
+
+ def as_json(opts = nil)
+ {
+ name: name,
+ namespace: namespace,
+ creation_timestamp: creation_timestamp,
+ manifest: manifest
+ }
+ end
+
+ private
+
+ attr_reader :name, :namespace, :creation_timestamp, :pod_selector, :policy_types, :ingress, :egress
+
+ def metadata
+ { name: name, namespace: namespace }
+ end
+
+ def spec
+ {
+ podSelector: pod_selector,
+ policyTypes: policy_types,
+ ingress: ingress,
+ egress: egress
+ }
+ end
+
+ def manifest
+ YAML.dump(metadata: metadata, spec: spec)
+ end
+ end
+ end
+end
diff --git a/spec/factories/design_management/versions.rb b/spec/factories/design_management/versions.rb
index 878665e02e5..e6d17ba691c 100644
--- a/spec/factories/design_management/versions.rb
+++ b/spec/factories/design_management/versions.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :design_version, class: 'DesignManagement::Version' do
- sequence(:sha) { |n| Digest::SHA1.hexdigest("commit-like-#{n}") }
+ sha
issue { designs.first&.issue || create(:issue) }
author { issue&.author || create(:user) }
diff --git a/spec/factories/git_wiki_commit_details.rb b/spec/factories/git_wiki_commit_details.rb
new file mode 100644
index 00000000000..b35f102fd4d
--- /dev/null
+++ b/spec/factories/git_wiki_commit_details.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :git_wiki_commit_details, class: 'Gitlab::Git::Wiki::CommitDetails' do
+ skip_create
+
+ transient do
+ author { create(:user) }
+ end
+
+ sequence(:message) { |n| "Commit message #{n}" }
+
+ initialize_with { new(author.id, author.username, author.name, author.email, message) }
+ end
+end
diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb
index cdc64a8502e..ca0804965df 100644
--- a/spec/factories/sequences.rb
+++ b/spec/factories/sequences.rb
@@ -12,4 +12,5 @@ FactoryBot.define do
sequence(:branch) { |n| "my-branch-#{n}" }
sequence(:past_time) { |n| 4.hours.ago + (2 * n).seconds }
sequence(:iid)
+ sequence(:sha) { |n| Digest::SHA1.hexdigest("commit-like-#{n}") }
end
diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb
index e5df970dc9c..e7fcc19bbfe 100644
--- a/spec/factories/wiki_pages.rb
+++ b/spec/factories/wiki_pages.rb
@@ -66,5 +66,6 @@ FactoryBot.define do
end
sequence(:wiki_page_title) { |n| "Page #{n}" }
+ sequence(:wiki_filename) { |n| "Page_#{n}.md" }
sequence(:sluggified_title) { |n| "slug-#{n}" }
end
diff --git a/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
new file mode 100644
index 00000000000..acdfb5139bf
--- /dev/null
+++ b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
@@ -0,0 +1,41 @@
+import { shallowMount } from '@vue/test-utils';
+import EditFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
+
+describe('Edit Form Buttons', () => {
+ let wrapper;
+ const findConfidentialToggle = () => wrapper.find('[data-testid="confidential-toggle"]');
+
+ const createComponent = props => {
+ wrapper = shallowMount(EditFormButtons, {
+ propsData: {
+ updateConfidentialAttribute: () => {},
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when not confidential', () => {
+ it('renders Turn On in the ', () => {
+ createComponent({
+ isConfidential: false,
+ });
+
+ expect(findConfidentialToggle().text()).toBe('Turn On');
+ });
+ });
+
+ describe('when confidential', () => {
+ it('renders on or off text based on confidentiality', () => {
+ createComponent({
+ isConfidential: true,
+ });
+
+ expect(findConfidentialToggle().text()).toBe('Turn Off');
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/confidential/edit_form_spec.js b/spec/frontend/sidebar/confidential/edit_form_spec.js
new file mode 100644
index 00000000000..137019a1e1b
--- /dev/null
+++ b/spec/frontend/sidebar/confidential/edit_form_spec.js
@@ -0,0 +1,45 @@
+import { shallowMount } from '@vue/test-utils';
+import EditForm from '~/sidebar/components/confidential/edit_form.vue';
+
+describe('Edit Form Dropdown', () => {
+ let wrapper;
+ const toggleForm = () => {};
+ const updateConfidentialAttribute = () => {};
+
+ const createComponent = props => {
+ wrapper = shallowMount(EditForm, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when not confidential', () => {
+ it('renders "You are going to turn off the confidentiality." in the ', () => {
+ createComponent({
+ isConfidential: false,
+ toggleForm,
+ updateConfidentialAttribute,
+ });
+
+ expect(wrapper.find('p').text()).toContain('You are going to turn on the confidentiality.');
+ });
+ });
+
+ describe('when confidential', () => {
+ it('renders on or off text based on confidentiality', () => {
+ createComponent({
+ isConfidential: true,
+ toggleForm,
+ updateConfidentialAttribute,
+ });
+
+ expect(wrapper.find('p').text()).toContain('You are going to turn off the confidentiality.');
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/confidential_edit_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_buttons_spec.js
deleted file mode 100644
index 32da9f83112..00000000000
--- a/spec/frontend/sidebar/confidential_edit_buttons_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Vue from 'vue';
-import editFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
-
-describe('Edit Form Buttons', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(editFormButtons);
- const toggleForm = () => {};
- const updateConfidentialAttribute = () => {};
-
- vm1 = new Component({
- propsData: {
- isConfidential: true,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
-
- vm2 = new Component({
- propsData: {
- isConfidential: false,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
- });
-
- it('renders on or off text based on confidentiality', () => {
- expect(vm1.$el.innerHTML.includes('Turn Off')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('Turn On')).toBe(true);
- });
-});
diff --git a/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js
deleted file mode 100644
index 369088cb258..00000000000
--- a/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Vue from 'vue';
-import editForm from '~/sidebar/components/confidential/edit_form.vue';
-
-describe('Edit Form Dropdown', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(editForm);
- const toggleForm = () => {};
- const updateConfidentialAttribute = () => {};
-
- vm1 = new Component({
- propsData: {
- isConfidential: true,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
-
- vm2 = new Component({
- propsData: {
- isConfidential: false,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
- });
-
- it('renders on the appropriate warning text', () => {
- expect(vm1.$el.innerHTML.includes('You are going to turn off the confidentiality.')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('You are going to turn on the confidentiality.')).toBe(true);
- });
-});
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index b2dbbbf59ac..8c6c91d919e 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -48,6 +48,7 @@ describe Gitlab::Ci::Config::Entry::Reports do
:cobertura | 'cobertura-coverage.xml'
:terraform | 'tfplan.json'
:accessibility | 'gl-accessibility.json'
+ :cluster_applications | 'gl-cluster-applications.json'
end
with_them do
diff --git a/spec/lib/gitlab/kubernetes/network_policy_spec.rb b/spec/lib/gitlab/kubernetes/network_policy_spec.rb
new file mode 100644
index 00000000000..87ed922e099
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/network_policy_spec.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::NetworkPolicy do
+ let(:policy) do
+ described_class.new(
+ name: name,
+ namespace: namespace,
+ creation_timestamp: '2020-04-14T00:08:30Z',
+ pod_selector: pod_selector,
+ policy_types: %w(Ingress Egress),
+ ingress: ingress,
+ egress: egress
+ )
+ end
+
+ let(:name) { 'example-name' }
+ let(:namespace) { 'example-namespace' }
+ let(:pod_selector) { { matchLabels: { role: 'db' } } }
+
+ let(:ingress) do
+ [
+ {
+ from: [
+ { namespaceSelector: { matchLabels: { project: 'myproject' } } }
+ ]
+ }
+ ]
+ end
+
+ let(:egress) do
+ [
+ {
+ ports: [{ port: 5978 }]
+ }
+ ]
+ end
+
+ describe '.from_yaml' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: example-name
+ namespace: example-namespace
+spec:
+ podSelector:
+ matchLabels:
+ role: db
+ policyTypes:
+ - Ingress
+ ingress:
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ project: myproject
+ POLICY
+ end
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+
+ subject { Gitlab::Kubernetes::NetworkPolicy.from_yaml(manifest)&.generate }
+
+ it { is_expected.to eq(resource) }
+
+ context 'with nil manifest' do
+ let(:manifest) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with invalid manifest' do
+ let(:manifest) { "\tfoo: bar" }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with manifest without metadata' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+spec:
+ podSelector:
+ matchLabels:
+ role: db
+ policyTypes:
+ - Ingress
+ ingress:
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ project: myproject
+ POLICY
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with manifest without spec' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: example-name
+ namespace: example-namespace
+ POLICY
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with disallowed class' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: example-name
+ namespace: example-namespace
+ creationTimestamp: 2020-04-14T00:08:30Z
+spec:
+ podSelector:
+ matchLabels:
+ role: db
+ policyTypes:
+ - Ingress
+ ingress:
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ project: myproject
+ POLICY
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '.from_resource' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace, creationTimestamp: '2020-04-14T00:08:30Z', resourceVersion: '4990' },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+ let(:generated_resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+
+ subject { Gitlab::Kubernetes::NetworkPolicy.from_resource(resource)&.generate }
+
+ it { is_expected.to eq(generated_resource) }
+
+ context 'with nil resource' do
+ let(:resource) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with resource without metadata' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with resource without spec' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace, uid: '128cf288-7de4-11ea-aceb-42010a800089', resourceVersion: '4990' }
+ )
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#generate' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress Egress), ingress: ingress, egress: egress }
+ )
+ end
+
+ subject { policy.generate }
+
+ it { is_expected.to eq(resource) }
+ end
+
+ describe '#as_json' do
+ let(:json_policy) do
+ {
+ name: name,
+ namespace: namespace,
+ creation_timestamp: '2020-04-14T00:08:30Z',
+ manifest: YAML.dump(
+ {
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress Egress), ingress: ingress, egress: egress }
+ }
+ )
+ }
+ end
+
+ subject { policy.as_json }
+
+ it { is_expected.to eq(json_policy) }
+ end
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 844cd1626ab..5e0c31c3293 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -84,6 +84,21 @@ describe Event do
end
end
+ describe 'scopes' do
+ describe 'created_at' do
+ it 'can find the right event' do
+ time = 1.day.ago
+ event = create(:event, created_at: time)
+ false_positive = create(:event, created_at: 2.days.ago)
+
+ found = described_class.created_at(time)
+
+ expect(found).to include(event)
+ expect(found).not_to include(false_positive)
+ end
+ end
+ end
+
describe "Push event" do
let(:project) { create(:project, :private) }
let(:user) { project.owner }
@@ -511,6 +526,14 @@ describe Event do
expect(described_class.not_wiki_page).to match_array(non_wiki_events)
end
end
+
+ describe '.for_wiki_meta' do
+ it 'finds events for a given wiki page metadata object' do
+ event = events.select(&:wiki_page?).first
+
+ expect(described_class.for_wiki_meta(event.target)).to contain_exactly(event)
+ end
+ end
end
describe '#wiki_page and #wiki_page?' do
diff --git a/spec/models/wiki_page/meta_spec.rb b/spec/models/wiki_page/meta_spec.rb
index f9bfc31ba64..0255dd802cf 100644
--- a/spec/models/wiki_page/meta_spec.rb
+++ b/spec/models/wiki_page/meta_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe WikiPage::Meta do
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, :wiki_repo) }
let_it_be(:other_project) { create(:project) }
describe 'Associations' do
@@ -169,8 +169,11 @@ describe WikiPage::Meta do
described_class.find_or_create(last_known_slug, wiki_page)
end
- def create_previous_version(title = old_title, slug = last_known_slug)
- create(:wiki_page_meta, title: title, project: project, canonical_slug: slug)
+ def create_previous_version(title: old_title, slug: last_known_slug, date: wiki_page.version.commit.committed_date)
+ create(:wiki_page_meta,
+ title: title, project: project,
+ created_at: date, updated_at: date,
+ canonical_slug: slug)
end
def create_context
@@ -198,6 +201,8 @@ describe WikiPage::Meta do
title: wiki_page.title,
project: wiki_page.wiki.project
)
+ expect(meta.updated_at).to eq(wiki_page.version.commit.committed_date)
+ expect(meta.created_at).not_to be_after(meta.updated_at)
expect(meta.slugs.where(slug: last_known_slug)).to exist
expect(meta.slugs.canonical.where(slug: wiki_page.slug)).to exist
end
@@ -209,22 +214,32 @@ describe WikiPage::Meta do
end
end
- context 'the slug is too long' do
- let(:last_known_slug) { FFaker::Lorem.characters(2050) }
+ context 'there are problems' do
+ context 'the slug is too long' do
+ let(:last_known_slug) { FFaker::Lorem.characters(2050) }
- it 'raises an error' do
- expect { find_record }.to raise_error ActiveRecord::ValueTooLong
+ it 'raises an error' do
+ expect { find_record }.to raise_error ActiveRecord::ValueTooLong
+ end
end
- end
- context 'a conflicting record exists' do
- before do
- create(:wiki_page_meta, project: project, canonical_slug: last_known_slug)
- create(:wiki_page_meta, project: project, canonical_slug: current_slug)
+ context 'a conflicting record exists' do
+ before do
+ create(:wiki_page_meta, project: project, canonical_slug: last_known_slug)
+ create(:wiki_page_meta, project: project, canonical_slug: current_slug)
+ end
+
+ it 'raises an error' do
+ expect { find_record }.to raise_error(ActiveRecord::RecordInvalid)
+ end
end
- it 'raises an error' do
- expect { find_record }.to raise_error(ActiveRecord::RecordInvalid)
+ context 'the wiki page is not valid' do
+ let(:wiki_page) { build(:wiki_page, project: project, title: nil) }
+
+ it 'raises an error' do
+ expect { find_record }.to raise_error(described_class::WikiPageInvalid)
+ end
end
end
@@ -258,6 +273,17 @@ describe WikiPage::Meta do
end
end
+ context 'the commit happened a day ago' do
+ before do
+ allow(wiki_page.version.commit).to receive(:committed_date).and_return(1.day.ago)
+ end
+
+ include_examples 'metadata examples' do
+ # Identical to the base case.
+ let(:query_limit) { 5 }
+ end
+ end
+
context 'the last_known_slug is the same as the current slug, as on creation' do
let(:last_known_slug) { current_slug }
@@ -292,6 +318,33 @@ describe WikiPage::Meta do
end
end
+ context 'a record exists in the DB, but we need to update timestamps' do
+ let(:last_known_slug) { current_slug }
+ let(:old_title) { title }
+
+ before do
+ create_previous_version(date: 1.week.ago)
+ end
+
+ include_examples 'metadata examples' do
+ # We need the query, and the update
+ # SAVEPOINT active_record_2
+ #
+ # SELECT * FROM wiki_page_meta
+ # INNER JOIN wiki_page_slugs
+ # ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id
+ # WHERE wiki_page_meta.project_id = ?
+ # AND wiki_page_slugs.canonical = TRUE
+ # AND wiki_page_slugs.slug = ?
+ # LIMIT 2
+ #
+ # UPDATE wiki_page_meta SET updated_at = ?date WHERE id = ?id
+ #
+ # RELEASE SAVEPOINT active_record_2
+ let(:query_limit) { 4 }
+ end
+ end
+
context 'we need to update the slug, but not the title' do
let(:old_title) { title }
@@ -359,14 +412,14 @@ describe WikiPage::Meta do
end
context 'we want to change the slug back to a previous version' do
- let(:slug_1) { 'foo' }
- let(:slug_2) { 'bar' }
+ let(:slug_1) { generate(:sluggified_title) }
+ let(:slug_2) { generate(:sluggified_title) }
let(:wiki_page) { create(:wiki_page, title: slug_1, project: project) }
let(:last_known_slug) { slug_2 }
before do
- meta = create_previous_version(title, slug_1)
+ meta = create_previous_version(title: title, slug: slug_1)
meta.canonical_slug = slug_2
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index eb241fa123f..305b67a4262 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -844,6 +844,20 @@ describe WikiPage do
end
end
+ describe '#version_commit_timestamp' do
+ context 'for a new page' do
+ it 'returns nil' do
+ expect(new_page.version_commit_timestamp).to be_nil
+ end
+ end
+
+ context 'for page that exists' do
+ it 'returns the timestamp of the commit' do
+ expect(existing_page.version_commit_timestamp).to eq(existing_page.version.commit.committed_date)
+ end
+ end
+ end
+
private
def get_slugs(page_or_dir)
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 09b76c57715..01b5ce981df 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -34,7 +34,7 @@ describe Ci::RetryBuildService do
job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_license_scanning
job_artifacts_performance job_artifacts_lsif
- job_artifacts_terraform
+ job_artifacts_terraform job_artifacts_cluster_applications
job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee job_artifacts_dotenv
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 0a8a4d5bf58..987b4ad68f7 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -162,16 +162,25 @@ describe EventCreateService do
context "The action is #{action}" do
let(:event) { service.wiki_event(meta, user, action) }
- it 'creates the event' do
+ it 'creates the event', :aggregate_failures do
expect(event).to have_attributes(
wiki_page?: true,
valid?: true,
persisted?: true,
action: action,
- wiki_page: wiki_page
+ wiki_page: wiki_page,
+ author: user
)
end
+ it 'is idempotent', :aggregate_failures do
+ expect { event }.to change(Event, :count).by(1)
+ duplicate = nil
+ expect { duplicate = service.wiki_event(meta, user, action) }.not_to change(Event, :count)
+
+ expect(duplicate).to eq(event)
+ end
+
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
diff --git a/spec/services/git/wiki_push_service/change_spec.rb b/spec/services/git/wiki_push_service/change_spec.rb
new file mode 100644
index 00000000000..547874270ab
--- /dev/null
+++ b/spec/services/git/wiki_push_service/change_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::WikiPushService::Change do
+ subject { described_class.new(project_wiki, change, raw_change) }
+
+ let(:project_wiki) { double('ProjectWiki') }
+ let(:raw_change) { double('RawChange', new_path: new_path, old_path: old_path, operation: operation) }
+ let(:change) { { oldrev: generate(:sha), newrev: generate(:sha) } }
+
+ let(:new_path) do
+ case operation
+ when :deleted
+ nil
+ else
+ generate(:wiki_filename)
+ end
+ end
+
+ let(:old_path) do
+ case operation
+ when :added
+ nil
+ when :deleted, :renamed
+ generate(:wiki_filename)
+ else
+ new_path
+ end
+ end
+
+ describe '#page' do
+ context 'the page does not exist' do
+ before do
+ expect(project_wiki).to receive(:find_page).with(String, String).and_return(nil)
+ end
+
+ %i[added deleted renamed modified].each do |op|
+ context "the operation is #{op}" do
+ let(:operation) { op }
+
+ it { is_expected.to have_attributes(page: be_nil) }
+ end
+ end
+ end
+
+ context 'the page can be found' do
+ let(:wiki_page) { double('WikiPage') }
+
+ before do
+ expect(project_wiki).to receive(:find_page).with(slug, revision).and_return(wiki_page)
+ end
+
+ context 'the page has been deleted' do
+ let(:operation) { :deleted }
+ let(:slug) { old_path.chomp('.md') }
+ let(:revision) { change[:oldrev] }
+
+ it { is_expected.to have_attributes(page: wiki_page) }
+ end
+
+ %i[added renamed modified].each do |op|
+ let(:operation) { op }
+ let(:slug) { new_path.chomp('.md') }
+ let(:revision) { change[:newrev] }
+
+ it { is_expected.to have_attributes(page: wiki_page) }
+ end
+ end
+ end
+
+ describe '#last_known_slug' do
+ context 'the page has been created' do
+ let(:operation) { :added }
+
+ it { is_expected.to have_attributes(last_known_slug: new_path.chomp('.md')) }
+ end
+
+ %i[renamed modified deleted].each do |op|
+ context "the operation is #{op}" do
+ let(:operation) { op }
+
+ it { is_expected.to have_attributes(last_known_slug: old_path.chomp('.md')) }
+ end
+ end
+ end
+
+ describe '#event_action' do
+ context 'the page is deleted' do
+ let(:operation) { :deleted }
+
+ it { is_expected.to have_attributes(event_action: Event::DESTROYED) }
+ end
+
+ context 'the page is added' do
+ let(:operation) { :added }
+
+ it { is_expected.to have_attributes(event_action: Event::CREATED) }
+ end
+
+ %i[renamed modified].each do |op|
+ context "the page is #{op}" do
+ let(:operation) { op }
+
+ it { is_expected.to have_attributes(event_action: Event::UPDATED) }
+ end
+ end
+ end
+end
diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb
new file mode 100644
index 00000000000..2f844b92a2a
--- /dev/null
+++ b/spec/services/git/wiki_push_service_spec.rb
@@ -0,0 +1,338 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::WikiPushService, services: true do
+ include RepoHelpers
+
+ let_it_be(:key_id) { create(:key, user: current_user).shell_id }
+ let_it_be(:project) { create(:project, :wiki_repo) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:git_wiki) { project.wiki.wiki }
+ let_it_be(:repository) { git_wiki.repository }
+
+ describe '#execute' do
+ context 'the push contains more than the permitted number of changes' do
+ def run_service
+ process_changes { described_class::MAX_CHANGES.succ.times { write_new_page } }
+ end
+
+ it 'creates only MAX_CHANGES events' do
+ expect { run_service }.to change(Event, :count).by(described_class::MAX_CHANGES)
+ end
+ end
+
+ context 'default_branch collides with a tag' do
+ it 'creates only one event' do
+ base_sha = current_sha
+ write_new_page
+
+ service = create_service(base_sha, ['refs/heads/master', 'refs/tags/master'])
+
+ expect { service.execute }.to change(Event, :count).by(1)
+ end
+ end
+
+ describe 'successfully creating events' do
+ let(:count) { Event::WIKI_ACTIONS.size }
+
+ def run_service
+ wiki_page_a = create(:wiki_page, project: project)
+ wiki_page_b = create(:wiki_page, project: project)
+
+ process_changes do
+ write_new_page
+ update_page(wiki_page_a.title)
+ delete_page(wiki_page_b.page.path)
+ end
+ end
+
+ it 'creates one event for every wiki action' do
+ expect { run_service }.to change(Event, :count).by(count)
+ end
+
+ it 'handles all known actions' do
+ run_service
+
+ expect(Event.last(count).pluck(:action)).to match_array(Event::WIKI_ACTIONS)
+ end
+ end
+
+ context 'two pages have been created' do
+ def run_service
+ process_changes do
+ write_new_page
+ write_new_page
+ end
+ end
+
+ it 'creates two events' do
+ expect { run_service }.to change(Event, :count).by(2)
+ end
+
+ it 'creates two metadata records' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(2)
+ end
+
+ it 'creates appropriate events' do
+ run_service
+
+ expect(Event.last(2)).to all(have_attributes(wiki_page?: true, action: Event::CREATED))
+ end
+ end
+
+ context 'a non-page file as been added' do
+ it 'does not create events, or WikiPage metadata' do
+ expect do
+ process_changes { write_non_page }
+ end.not_to change { [Event.count, WikiPage::Meta.count] }
+ end
+ end
+
+ context 'one page, and one non-page have been created' do
+ def run_service
+ process_changes do
+ write_new_page
+ write_non_page
+ end
+ end
+
+ it 'creates a wiki page creation event' do
+ expect { run_service }.to change(Event, :count).by(1)
+
+ expect(Event.last).to have_attributes(wiki_page?: true, action: Event::CREATED)
+ end
+
+ it 'creates one metadata record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+ end
+
+ context 'one page has been added, and then updated' do
+ def run_service
+ process_changes do
+ title = write_new_page
+ update_page(title)
+ end
+ end
+
+ it 'creates just a single event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'creates just one metadata record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+
+ it 'creates a new wiki page creation event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::CREATED
+ )
+ end
+ end
+
+ context 'when a page we already know about has been updated' do
+ let(:wiki_page) { create(:wiki_page, project: project) }
+
+ before do
+ create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page)
+ end
+
+ def run_service
+ process_changes { update_page(wiki_page.title) }
+ end
+
+ it 'does not create a new meta-data record' do
+ expect { run_service }.not_to change(WikiPage::Meta, :count)
+ end
+
+ it 'creates a new event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'adds an update event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::UPDATED
+ )
+ end
+ end
+
+ context 'when a page we do not know about has been updated' do
+ def run_service
+ wiki_page = create(:wiki_page, project: project)
+ process_changes { update_page(wiki_page.title) }
+ end
+
+ it 'creates a new meta-data record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+
+ it 'creates a new event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'adds an update event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::UPDATED
+ )
+ end
+ end
+
+ context 'when a page we do not know about has been deleted' do
+ def run_service
+ wiki_page = create(:wiki_page, project: project)
+ process_changes { delete_page(wiki_page.page.path) }
+ end
+
+ it 'create a new meta-data record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+
+ it 'creates a new event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'adds an update event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::DESTROYED
+ )
+ end
+ end
+
+ it 'calls log_error for every event we cannot create' do
+ base_sha = current_sha
+ count = 3
+ count.times { write_new_page }
+ message = 'something went very very wrong'
+ allow_next_instance_of(WikiPages::EventCreateService, current_user) do |service|
+ allow(service).to receive(:execute)
+ .with(String, WikiPage, Integer)
+ .and_return(ServiceResponse.error(message: message))
+ end
+
+ service = create_service(base_sha)
+
+ expect(service).to receive(:log_error).exactly(count).times.with(message)
+
+ service.execute
+ end
+
+ describe 'feature flags' do
+ shared_examples 'a no-op push' do
+ it 'does not create any events' do
+ expect { process_changes { write_new_page } }.not_to change(Event, :count)
+ end
+
+ it 'does not even look for events to process' do
+ base_sha = current_sha
+ write_new_page
+
+ service = create_service(base_sha)
+
+ expect(service).not_to receive(:changed_files)
+
+ service.execute
+ end
+ end
+
+ context 'the wiki_events feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it_behaves_like 'a no-op push'
+ end
+
+ context 'the wiki_events_on_git_push feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events_on_git_push: false)
+ end
+
+ it_behaves_like 'a no-op push'
+
+ context 'but is enabled for a given project' do
+ before do
+ stub_feature_flags(wiki_events_on_git_push: { enabled: true, thing: project })
+ end
+
+ it 'creates events' do
+ expect { process_changes { write_new_page } }.to change(Event, :count).by(1)
+ end
+ end
+ end
+ end
+ end
+
+ # In order to construct the correct GitPostReceive object that represents the
+ # changes we are applying, we need to describe the changes between old-ref and
+ # new-ref. Old ref (the base sha) we have to capture before we perform any
+ # changes. Once the changes have been applied, we can execute the service to
+ # process them.
+ def process_changes(&block)
+ base_sha = current_sha
+ yield
+ create_service(base_sha).execute
+ end
+
+ def create_service(base, refs = ['refs/heads/master'])
+ changes = post_received(base, refs).changes
+ described_class.new(project, current_user, changes: changes)
+ end
+
+ def post_received(base, refs)
+ change_str = refs.map { |ref| +"#{base} #{current_sha} #{ref}" }.join("\n")
+ post_received = ::Gitlab::GitPostReceive.new(project, key_id, change_str, {})
+ allow(post_received).to receive(:identify).with(key_id).and_return(current_user)
+
+ post_received
+ end
+
+ def current_sha
+ repository.gitaly_ref_client.find_branch('master')&.dereferenced_target&.id || Gitlab::Git::BLANK_SHA
+ end
+
+ # It is important not to re-use the WikiPage services here, since they create
+ # events - these helper methods below are intended to simulate actions on the repo
+ # that have not gone through our services.
+
+ def write_new_page
+ generate(:wiki_page_title).tap { |t| git_wiki.write_page(t, 'markdown', 'Hello', commit_details) }
+ end
+
+ # We write something to the wiki-repo that is not a page - as, for example, an
+ # attachment. This will appear as a raw-diff change, but wiki.find_page will
+ # return nil.
+ def write_non_page
+ params = {
+ file_name: 'attachment.log',
+ file_content: 'some stuff',
+ branch_name: 'master'
+ }
+ ::Wikis::CreateAttachmentService.new(project, project.owner, params).execute
+ end
+
+ def update_page(title)
+ page = git_wiki.page(title: title)
+ git_wiki.update_page(page.path, title, 'markdown', 'Hey', commit_details)
+ end
+
+ def delete_page(path)
+ git_wiki.delete_page(path, commit_details)
+ end
+
+ def commit_details
+ create(:git_wiki_commit_details, author: current_user)
+ end
+end
diff --git a/spec/services/wiki_pages/event_create_service_spec.rb b/spec/services/wiki_pages/event_create_service_spec.rb
new file mode 100644
index 00000000000..cf971b0a02c
--- /dev/null
+++ b/spec/services/wiki_pages/event_create_service_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe WikiPages::EventCreateService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ subject { described_class.new(user) }
+
+ describe '#execute' do
+ let_it_be(:page) { create(:wiki_page, project: project) }
+ let(:slug) { generate(:sluggified_title) }
+ let(:action) { Event::CREATED }
+ let(:response) { subject.execute(slug, page, action) }
+
+ context 'feature flag is not enabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it 'does not error' do
+ expect(response).to be_success
+ .and have_attributes(message: /No event created/)
+ end
+
+ it 'does not create an event' do
+ expect { response }.not_to change(Event, :count)
+ end
+ end
+
+ context 'the user is nil' do
+ subject { described_class.new(nil) }
+
+ it 'raises an error on construction' do
+ expect { subject }.to raise_error ArgumentError
+ end
+ end
+
+ context 'the action is illegal' do
+ let(:action) { Event::WIKI_ACTIONS.max + 1 }
+
+ it 'returns an error' do
+ expect(response).to be_error
+ end
+
+ it 'does not create an event' do
+ expect { response }.not_to change(Event, :count)
+ end
+
+ it 'does not create a metadata record' do
+ expect { response }.not_to change(WikiPage::Meta, :count)
+ end
+ end
+
+ it 'returns a successful response' do
+ expect(response).to be_success
+ end
+
+ context 'the action is a deletion' do
+ let(:action) { Event::DESTROYED }
+
+ it 'does not synchronize the wiki metadata timestamps with the git commit' do
+ expect_next_instance_of(WikiPage::Meta) do |instance|
+ expect(instance).not_to receive(:synch_times_with_page)
+ end
+
+ response
+ end
+ end
+
+ it 'creates a wiki page event' do
+ expect { response }.to change(Event, :count).by(1)
+ end
+
+ it 'returns an event in the payload' do
+ expect(response.payload).to include(event: have_attributes(author: user, wiki_page?: true, action: action))
+ end
+
+ it 'records the slug for the page' do
+ response
+ meta = WikiPage::Meta.find_or_create(page.slug, page)
+
+ expect(meta.slugs.pluck(:slug)).to include(slug)
+ end
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 3d24b5f753a..3ad8eced2b3 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -299,6 +299,31 @@ describe PostReceive do
end
end
+ context "master" do
+ let(:default_branch) { 'master' }
+ let(:oldrev) { '012345' }
+ let(:newrev) { '6789ab' }
+ let(:changes) do
+ <<~EOF
+ #{oldrev} #{newrev} refs/heads/#{default_branch}
+ 123456 789012 refs/heads/tést2
+ EOF
+ end
+
+ let(:raw_repo) { double('RawRepo') }
+
+ it 'processes the changes on the master branch' do
+ expect_next_instance_of(Git::WikiPushService) do |service|
+ expect(service).to receive(:process_changes).and_call_original
+ end
+ expect(project.wiki).to receive(:default_branch).twice.and_return(default_branch)
+ expect(project.wiki.repository).to receive(:raw).and_return(raw_repo)
+ expect(raw_repo).to receive(:raw_changes_between).once.with(oldrev, newrev).and_return([])
+
+ perform
+ end
+ end
+
context "branches" do
let(:changes) do
<<~EOF
@@ -307,6 +332,12 @@ describe PostReceive do
EOF
end
+ before do
+ allow_next_instance_of(Git::WikiPushService) do |service|
+ allow(service).to receive(:process_changes)
+ end
+ end
+
it 'expires the branches cache' do
expect(project.wiki.repository).to receive(:expire_branches_cache).once