diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 4acbb0482f3..f96e080ecf8 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -38,6 +38,8 @@ class GroupsController < Groups::ApplicationController
before_action :check_export_rate_limit!, only: [:export, :download_export]
+ before_action :track_experiment_event, only: [:new]
+
helper_method :captcha_required?
skip_cross_project_access_check :index, :new, :create, :edit, :update,
@@ -378,6 +380,12 @@ class GroupsController < Groups::ApplicationController
def captcha_required?
captcha_enabled? && !params[:parent_id]
end
+
+ def track_experiment_event
+ return if params[:parent_id]
+
+ experiment(:require_verification_for_namespace_creation, user: current_user).track(:start_create_group)
+ end
end
GroupsController.prepend_mod_with('GroupsController')
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 89e87c4345e..c459afbbcf6 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -71,6 +71,7 @@ class Projects::IssuesController < Projects::ApplicationController
]
feature_category :service_desk, [:service_desk]
+ urgency :low, [:service_desk]
feature_category :importers, [:import_csv, :export_csv]
attr_accessor :vulnerability_id
diff --git a/app/controllers/projects/service_desk_controller.rb b/app/controllers/projects/service_desk_controller.rb
index 1fb07c3a903..aa0e70121df 100644
--- a/app/controllers/projects/service_desk_controller.rb
+++ b/app/controllers/projects/service_desk_controller.rb
@@ -4,6 +4,7 @@ class Projects::ServiceDeskController < Projects::ApplicationController
before_action :authorize_admin_project!
feature_category :service_desk
+ urgency :low
def show
json_response
diff --git a/app/experiments/require_verification_for_namespace_creation_experiment.rb b/app/experiments/require_verification_for_namespace_creation_experiment.rb
index 1cadac7e7d4..78390ddd099 100644
--- a/app/experiments/require_verification_for_namespace_creation_experiment.rb
+++ b/app/experiments/require_verification_for_namespace_creation_experiment.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
+ exclude :existing_user
+
+ EXPERIMENT_START_DATE = Date.new(2022, 1, 31)
+
def control_behavior
false
end
@@ -24,4 +28,10 @@ class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment
def subject
context.value[:user]
end
+
+ def existing_user
+ return false unless user_or_actor
+
+ user_or_actor.created_at < EXPERIMENT_START_DATE
+ end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 7296560a450..3fbea0c0472 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -139,7 +139,7 @@ module GroupsHelper
{}
end
- def require_verification_for_group_creation_enabled?
+ def require_verification_for_namespace_creation_enabled?
# overridden in EE
false
end
diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml
index 090cfc652ee..81403fd88b2 100644
--- a/app/views/groups/settings/_export.html.haml
+++ b/app/views/groups/settings/_export.html.haml
@@ -27,10 +27,10 @@
%li= _('Runner tokens')
%li= _('SAML discovery tokens')
- if group.export_file_exists?
- = link_to _('Regenerate export'), export_group_path(group),
- method: :post, class: 'btn gl-button btn-default', data: { qa_selector: 'regenerate_export_group_link' }
= link_to _('Download export'), download_export_group_path(group),
rel: 'nofollow', method: :get, class: 'btn gl-button btn-default', data: { qa_selector: 'download_export_link' }
+ = link_to _('Regenerate export'), export_group_path(group),
+ method: :post, class: 'btn gl-button btn-default', data: { qa_selector: 'regenerate_export_group_link' }
- else
= link_to _('Export group'), export_group_path(group),
method: :post, class: 'btn gl-button btn-default', data: { qa_selector: 'export_group_link' }
diff --git a/config/feature_flags/development/rate_limit_gitlab_shell.yml b/config/feature_flags/development/rate_limit_gitlab_shell.yml
index ceb9e86b01c..3c29a71af6e 100644
--- a/config/feature_flags/development/rate_limit_gitlab_shell.yml
+++ b/config/feature_flags/development/rate_limit_gitlab_shell.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350465
milestone: '14.7'
type: development
group: group::source code
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/experiment/require_verification_for_group_creation.yml b/config/feature_flags/experiment/require_verification_for_group_creation.yml
deleted file mode 100644
index 767d5f55bce..00000000000
--- a/config/feature_flags/experiment/require_verification_for_group_creation.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: require_verification_for_group_creation
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77569
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349857
-milestone: '14.7'
-type: experiment
-group: group::activation
-default_enabled: false
diff --git a/data/deprecations/15-0-instance-statistics-graphql-node-removal.yml b/data/deprecations/15-0-instance-statistics-graphql-node-removal.yml
new file mode 100644
index 00000000000..c617a6cc478
--- /dev/null
+++ b/data/deprecations/15-0-instance-statistics-graphql-node-removal.yml
@@ -0,0 +1,13 @@
+- name: "Querying Usage Trends via the `instanceStatisticsMeasurements` GraphQL node"
+ announcement_milestone: "14.8"
+ announcement_date: "2022-02-22"
+ removal_milestone: "15.0"
+ removal_date: "2022-05-22"
+ breaking_change: true
+ body: | # Do not modify this line, instead modify the lines below.
+ The `instanceStatisticsMeasurements` GraphQL node has been renamed to `usageTrendsMeasurements` in 13.10 and the old field name has been marked as deprecated. To fix the existing GraphQL queries, replace `instanceStatisticsMeasurements` with `usageTrendsMeasurements`.
+# The following items are not published on the docs page, but may be used in the future.
+ stage: Manage
+ tiers: [FREE]
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332323
+ documentation_url: https://docs.gitlab.com/ee/api/graphql/reference/index.html#queryusagetrendsmeasurements
diff --git a/db/migrate/20220106111958_add_insert_or_update_vulnerability_reads_trigger.rb b/db/migrate/20220106111958_add_insert_or_update_vulnerability_reads_trigger.rb
new file mode 100644
index 00000000000..0049f4e00a2
--- /dev/null
+++ b/db/migrate/20220106111958_add_insert_or_update_vulnerability_reads_trigger.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+class AddInsertOrUpdateVulnerabilityReadsTrigger < Gitlab::Database::Migration[1.0]
+ include Gitlab::Database::SchemaHelpers
+
+ FUNCTION_NAME = 'insert_or_update_vulnerability_reads'
+ TRIGGER_NAME = 'trigger_insert_or_update_vulnerability_reads_from_occurrences'
+
+ def up
+ execute(<<~SQL)
+ CREATE OR REPLACE FUNCTION #{FUNCTION_NAME}()
+ RETURNS TRIGGER
+ LANGUAGE plpgsql
+ AS $$
+ DECLARE
+ severity smallint;
+ state smallint;
+ report_type smallint;
+ resolved_on_default_branch boolean;
+ BEGIN
+ IF (NEW.vulnerability_id IS NULL AND (TG_OP = 'INSERT' OR TG_OP = 'UPDATE')) THEN
+ RETURN NULL;
+ END IF;
+
+ IF (TG_OP = 'UPDATE' AND OLD.vulnerability_id IS NOT NULL AND NEW.vulnerability_id IS NOT NULL) THEN
+ RETURN NULL;
+ END IF;
+
+ SELECT
+ vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch
+ INTO
+ severity, state, report_type, resolved_on_default_branch
+ FROM
+ vulnerabilities
+ WHERE
+ vulnerabilities.id = NEW.vulnerability_id;
+
+ INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id)
+ VALUES (NEW.vulnerability_id, NEW.project_id, NEW.scanner_id, report_type, severity, state, resolved_on_default_branch, NEW.uuid::uuid, NEW.location->>'image', NEW.location->'kubernetes_resource'->>'agent_id')
+ ON CONFLICT(vulnerability_id) DO NOTHING;
+ RETURN NULL;
+ END
+ $$;
+ SQL
+
+ execute(<<~SQL)
+ CREATE TRIGGER #{TRIGGER_NAME}
+ AFTER INSERT OR UPDATE ON vulnerability_occurrences
+ FOR EACH ROW
+ EXECUTE PROCEDURE #{FUNCTION_NAME}();
+ SQL
+ end
+
+ def down
+ drop_trigger(:vulnerability_occurrences, TRIGGER_NAME)
+ drop_function(FUNCTION_NAME)
+ end
+end
diff --git a/db/migrate/20220106112043_add_update_vulnerability_reads_trigger.rb b/db/migrate/20220106112043_add_update_vulnerability_reads_trigger.rb
new file mode 100644
index 00000000000..940ec638924
--- /dev/null
+++ b/db/migrate/20220106112043_add_update_vulnerability_reads_trigger.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class AddUpdateVulnerabilityReadsTrigger < Gitlab::Database::Migration[1.0]
+ include Gitlab::Database::SchemaHelpers
+
+ TRIGGER_NAME = 'trigger_update_vulnerability_reads_on_vulnerability_update'
+ FUNCTION_NAME = 'update_vulnerability_reads_from_vulnerability'
+
+ def up
+ create_trigger_function(FUNCTION_NAME, replace: true) do
+ <<~SQL
+ UPDATE
+ vulnerability_reads
+ SET
+ severity = NEW.severity,
+ state = NEW.state,
+ resolved_on_default_branch = NEW.resolved_on_default_branch
+ WHERE vulnerability_id = NEW.id;
+ RETURN NULL;
+ SQL
+ end
+
+ execute(<<~SQL)
+ CREATE TRIGGER #{TRIGGER_NAME}
+ AFTER UPDATE ON vulnerabilities
+ FOR EACH ROW
+ WHEN (
+ OLD.severity IS DISTINCT FROM NEW.severity OR
+ OLD.state IS DISTINCT FROM NEW.state OR
+ OLD.resolved_on_default_branch IS DISTINCT FROM NEW.resolved_on_default_branch
+ )
+ EXECUTE PROCEDURE #{FUNCTION_NAME}();
+ SQL
+ end
+
+ def down
+ drop_trigger(:vulnerabilities, TRIGGER_NAME)
+ drop_function(FUNCTION_NAME)
+ end
+end
diff --git a/db/migrate/20220106112085_add_update_vulnerability_reads_location_trigger.rb b/db/migrate/20220106112085_add_update_vulnerability_reads_location_trigger.rb
new file mode 100644
index 00000000000..a863fe8b7b8
--- /dev/null
+++ b/db/migrate/20220106112085_add_update_vulnerability_reads_location_trigger.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+class AddUpdateVulnerabilityReadsLocationTrigger < Gitlab::Database::Migration[1.0]
+ include Gitlab::Database::SchemaHelpers
+
+ TRIGGER_NAME = 'trigger_update_location_on_vulnerability_occurrences_update'
+ FUNCTION_NAME = 'update_location_from_vulnerability_occurrences'
+
+ def up
+ create_trigger_function(FUNCTION_NAME, replace: true) do
+ <<~SQL
+ UPDATE
+ vulnerability_reads
+ SET
+ location_image = NEW.location->>'image',
+ cluster_agent_id = NEW.location->'kubernetes_resource'->>'agent_id'
+ WHERE
+ vulnerability_id = NEW.vulnerability_id;
+ RETURN NULL;
+ SQL
+ end
+
+ execute(<<~SQL)
+ CREATE TRIGGER #{TRIGGER_NAME}
+ AFTER UPDATE ON vulnerability_occurrences
+ FOR EACH ROW
+ WHEN (
+ NEW.report_type IN (2, 7) AND (
+ OLD.location->>'image' IS DISTINCT FROM NEW.location->>'image' OR
+ OLD.location->'kubernetes_resource'->>'agent_id' IS DISTINCT FROM NEW.location->'kubernetes_resource'->>'agent_id'
+ )
+ )
+ EXECUTE PROCEDURE #{FUNCTION_NAME}();
+ SQL
+ end
+
+ def down
+ drop_trigger(:vulnerability_occurrences, TRIGGER_NAME)
+ drop_function(FUNCTION_NAME)
+ end
+end
diff --git a/db/migrate/20220106163326_add_has_issues_on_vulnerability_reads_trigger.rb b/db/migrate/20220106163326_add_has_issues_on_vulnerability_reads_trigger.rb
new file mode 100644
index 00000000000..b3023a1f915
--- /dev/null
+++ b/db/migrate/20220106163326_add_has_issues_on_vulnerability_reads_trigger.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+class AddHasIssuesOnVulnerabilityReadsTrigger < Gitlab::Database::Migration[1.0]
+ include Gitlab::Database::SchemaHelpers
+
+ TRIGGER_ON_INSERT = 'trigger_update_has_issues_on_vulnerability_issue_links_update'
+ INSERT_FUNCTION_NAME = 'set_has_issues_on_vulnerability_reads'
+
+ TRIGGER_ON_DELETE = 'trigger_update_has_issues_on_vulnerability_issue_links_delete'
+ DELETE_FUNCTION_NAME = 'unset_has_issues_on_vulnerability_reads'
+
+ def up
+ create_trigger_function(INSERT_FUNCTION_NAME, replace: true) do
+ <<~SQL
+ UPDATE
+ vulnerability_reads
+ SET
+ has_issues = true
+ WHERE
+ vulnerability_id = NEW.vulnerability_id AND has_issues IS FALSE;
+ RETURN NULL;
+ SQL
+ end
+
+ execute(<<~SQL)
+ CREATE OR REPLACE FUNCTION #{DELETE_FUNCTION_NAME}()
+ RETURNS TRIGGER
+ LANGUAGE plpgsql
+ AS $$
+ DECLARE
+ has_issue_links integer;
+ BEGIN
+ PERFORM 1
+ FROM
+ vulnerability_reads
+ WHERE
+ vulnerability_id = OLD.vulnerability_id
+ FOR UPDATE;
+
+ SELECT 1 INTO has_issue_links FROM vulnerability_issue_links WHERE vulnerability_id = OLD.vulnerability_id LIMIT 1;
+
+ IF (has_issue_links = 1) THEN
+ RETURN NULL;
+ END IF;
+
+ UPDATE
+ vulnerability_reads
+ SET
+ has_issues = false
+ WHERE
+ vulnerability_id = OLD.vulnerability_id;
+
+ RETURN NULL;
+ END
+ $$;
+ SQL
+
+ execute(<<~SQL)
+ CREATE TRIGGER #{TRIGGER_ON_INSERT}
+ AFTER INSERT ON vulnerability_issue_links
+ FOR EACH ROW
+ EXECUTE FUNCTION #{INSERT_FUNCTION_NAME}();
+ SQL
+
+ execute(<<~SQL)
+ CREATE TRIGGER #{TRIGGER_ON_DELETE}
+ AFTER DELETE ON vulnerability_issue_links
+ FOR EACH ROW
+ EXECUTE FUNCTION #{DELETE_FUNCTION_NAME}();
+ SQL
+ end
+
+ def down
+ drop_trigger(:vulnerability_issue_links, TRIGGER_ON_INSERT)
+ drop_function(INSERT_FUNCTION_NAME)
+ drop_trigger(:vulnerability_issue_links, TRIGGER_ON_DELETE)
+ drop_function(DELETE_FUNCTION_NAME)
+ end
+end
diff --git a/db/schema_migrations/20220106111958 b/db/schema_migrations/20220106111958
new file mode 100644
index 00000000000..954db532950
--- /dev/null
+++ b/db/schema_migrations/20220106111958
@@ -0,0 +1 @@
+c1af9546bdfa0f32c3c2faf362062cd300800514e5b1efd1fa8a1770753d00e5
\ No newline at end of file
diff --git a/db/schema_migrations/20220106112043 b/db/schema_migrations/20220106112043
new file mode 100644
index 00000000000..34c8c5152da
--- /dev/null
+++ b/db/schema_migrations/20220106112043
@@ -0,0 +1 @@
+8b51ae2b13066a56d2131efb7ea746335513031e751fb231e43121552d6f2b1d
\ No newline at end of file
diff --git a/db/schema_migrations/20220106112085 b/db/schema_migrations/20220106112085
new file mode 100644
index 00000000000..171f893a0ab
--- /dev/null
+++ b/db/schema_migrations/20220106112085
@@ -0,0 +1 @@
+f385631d0317630661d487011a228501a6cbc71de25ca457d75e6a815c598045
\ No newline at end of file
diff --git a/db/schema_migrations/20220106163326 b/db/schema_migrations/20220106163326
new file mode 100644
index 00000000000..dbfb9079dc1
--- /dev/null
+++ b/db/schema_migrations/20220106163326
@@ -0,0 +1 @@
+4726d84ff42e64b1c47c5ba454ff5be05f434a86bb2af4bbe27dd00e5e3da5cb
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 6e0d49078e2..085ce257403 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -45,6 +45,39 @@ RETURN NULL;
END
$$;
+CREATE FUNCTION insert_or_update_vulnerability_reads() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+DECLARE
+ severity smallint;
+ state smallint;
+ report_type smallint;
+ resolved_on_default_branch boolean;
+BEGIN
+ IF (NEW.vulnerability_id IS NULL AND (TG_OP = 'INSERT' OR TG_OP = 'UPDATE')) THEN
+ RETURN NULL;
+ END IF;
+
+ IF (TG_OP = 'UPDATE' AND OLD.vulnerability_id IS NOT NULL AND NEW.vulnerability_id IS NOT NULL) THEN
+ RETURN NULL;
+ END IF;
+
+ SELECT
+ vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch
+ INTO
+ severity, state, report_type, resolved_on_default_branch
+ FROM
+ vulnerabilities
+ WHERE
+ vulnerabilities.id = NEW.vulnerability_id;
+
+ INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id)
+ VALUES (NEW.vulnerability_id, NEW.project_id, NEW.scanner_id, report_type, severity, state, resolved_on_default_branch, NEW.uuid::uuid, NEW.location->>'image', NEW.location->'kubernetes_resource'->>'agent_id')
+ ON CONFLICT(vulnerability_id) DO NOTHING;
+ RETURN NULL;
+END
+$$;
+
CREATE FUNCTION insert_projects_sync_event() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -107,6 +140,83 @@ RETURN NULL;
END
$$;
+CREATE FUNCTION set_has_issues_on_vulnerability_reads() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+BEGIN
+UPDATE
+ vulnerability_reads
+SET
+ has_issues = true
+WHERE
+ vulnerability_id = NEW.vulnerability_id AND has_issues IS FALSE;
+RETURN NULL;
+
+END
+$$;
+
+CREATE FUNCTION unset_has_issues_on_vulnerability_reads() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+DECLARE
+ has_issue_links integer;
+BEGIN
+ PERFORM 1
+ FROM
+ vulnerability_reads
+ WHERE
+ vulnerability_id = OLD.vulnerability_id
+ FOR UPDATE;
+
+ SELECT 1 INTO has_issue_links FROM vulnerability_issue_links WHERE vulnerability_id = OLD.vulnerability_id LIMIT 1;
+
+ IF (has_issue_links = 1) THEN
+ RETURN NULL;
+ END IF;
+
+ UPDATE
+ vulnerability_reads
+ SET
+ has_issues = false
+ WHERE
+ vulnerability_id = OLD.vulnerability_id;
+
+ RETURN NULL;
+END
+$$;
+
+CREATE FUNCTION update_location_from_vulnerability_occurrences() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+BEGIN
+UPDATE
+ vulnerability_reads
+SET
+ location_image = NEW.location->>'image',
+ cluster_agent_id = NEW.location->'kubernetes_resource'->>'agent_id'
+WHERE
+ vulnerability_id = NEW.vulnerability_id;
+RETURN NULL;
+
+END
+$$;
+
+CREATE FUNCTION update_vulnerability_reads_from_vulnerability() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+BEGIN
+UPDATE
+ vulnerability_reads
+SET
+ severity = NEW.severity,
+ state = NEW.state,
+ resolved_on_default_branch = NEW.resolved_on_default_branch
+WHERE vulnerability_id = NEW.id;
+RETURN NULL;
+
+END
+$$;
+
CREATE TABLE audit_events (
id bigint NOT NULL,
author_id integer NOT NULL,
@@ -29163,6 +29273,8 @@ CREATE TRIGGER trigger_has_external_wiki_on_type_new_updated AFTER UPDATE OF typ
CREATE TRIGGER trigger_has_external_wiki_on_update AFTER UPDATE ON integrations FOR EACH ROW WHEN (((new.type_new = 'Integrations::ExternalWiki'::text) AND (old.active <> new.active) AND (new.project_id IS NOT NULL))) EXECUTE FUNCTION set_has_external_wiki();
+CREATE TRIGGER trigger_insert_or_update_vulnerability_reads_from_occurrences AFTER INSERT OR UPDATE ON vulnerability_occurrences FOR EACH ROW EXECUTE FUNCTION insert_or_update_vulnerability_reads();
+
CREATE TRIGGER trigger_namespaces_parent_id_on_insert AFTER INSERT ON namespaces FOR EACH ROW EXECUTE FUNCTION insert_namespaces_sync_event();
CREATE TRIGGER trigger_namespaces_parent_id_on_update AFTER UPDATE ON namespaces FOR EACH ROW WHEN ((old.parent_id IS DISTINCT FROM new.parent_id)) EXECUTE FUNCTION insert_namespaces_sync_event();
@@ -29173,6 +29285,14 @@ CREATE TRIGGER trigger_projects_parent_id_on_update AFTER UPDATE ON projects FOR
CREATE TRIGGER trigger_type_new_on_insert AFTER INSERT ON integrations FOR EACH ROW EXECUTE FUNCTION integrations_set_type_new();
+CREATE TRIGGER trigger_update_has_issues_on_vulnerability_issue_links_delete AFTER DELETE ON vulnerability_issue_links FOR EACH ROW EXECUTE FUNCTION unset_has_issues_on_vulnerability_reads();
+
+CREATE TRIGGER trigger_update_has_issues_on_vulnerability_issue_links_update AFTER INSERT ON vulnerability_issue_links FOR EACH ROW EXECUTE FUNCTION set_has_issues_on_vulnerability_reads();
+
+CREATE TRIGGER trigger_update_location_on_vulnerability_occurrences_update AFTER UPDATE ON vulnerability_occurrences FOR EACH ROW WHEN (((new.report_type = ANY (ARRAY[2, 7])) AND (((old.location ->> 'image'::text) IS DISTINCT FROM (new.location ->> 'image'::text)) OR (((old.location -> 'kubernetes_resource'::text) ->> 'agent_id'::text) IS DISTINCT FROM ((new.location -> 'kubernetes_resource'::text) ->> 'agent_id'::text))))) EXECUTE FUNCTION update_location_from_vulnerability_occurrences();
+
+CREATE TRIGGER trigger_update_vulnerability_reads_on_vulnerability_update AFTER UPDATE ON vulnerabilities FOR EACH ROW WHEN (((old.severity IS DISTINCT FROM new.severity) OR (old.state IS DISTINCT FROM new.state) OR (old.resolved_on_default_branch IS DISTINCT FROM new.resolved_on_default_branch))) EXECUTE FUNCTION update_vulnerability_reads_from_vulnerability();
+
CREATE TRIGGER users_loose_fk_trigger AFTER DELETE ON users REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
ALTER TABLE ONLY chat_names
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index da304b7154b..c57d24986c6 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -721,6 +721,18 @@ The `merged_by` field in the [merge request API](https://docs.gitlab.com/ee/api/
## 14.8
+### Querying Usage Trends via the `instanceStatisticsMeasurements` GraphQL node
+
+WARNING:
+This feature will be changed or removed in 15.0
+as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes).
+Before updating GitLab, review the details carefully to determine if you need to make any
+changes to your code, settings, or workflow.
+
+The `instanceStatisticsMeasurements` GraphQL node has been renamed to `usageTrendsMeasurements` in 13.10 and the old field name has been marked as deprecated. To fix the existing GraphQL queries, replace `instanceStatisticsMeasurements` with `usageTrendsMeasurements`.
+
+**Planned removal milestone: 15.0 (2022-05-22)**
+
### REST and GraphQL API Runner usage of `active` replaced by `paused`
WARNING:
diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md
index bfc1097d5b4..6e62e533b1e 100644
--- a/doc/user/project/working_with_projects.md
+++ b/doc/user/project/working_with_projects.md
@@ -297,16 +297,16 @@ To delete a project:
1. Select **Delete project**
1. Confirm this action by completing the field.
-## Projects pending deletion **(PREMIUM SELF)**
+## Projects pending deletion **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37014) in GitLab 13.3 for Administrators.
> - [Tab renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/347468) from **Deleted projects** in GitLab 14.6.
-> - [Available to all users](https://gitlab.com/gitlab-org/gitlab/-/issues/346976) in GitLab 14.8 [with a flag](../../administration/feature_flags.md) named `project_owners_list_project_pending_deletion`. Disabled by default.
+> - [Available to all users](https://gitlab.com/gitlab-org/gitlab/-/issues/346976) in GitLab 14.8 [with a flag](../../administration/feature_flags.md) named `project_owners_list_project_pending_deletion`. Enabled by default.
FLAG:
-On self-managed GitLab, by default this feature is available to administrators only. To make it available to all users,
-ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `project_owners_list_project_pending_deletion`.
-On GitLab.com, this feature is available to GitLab.com administrators only. The feature being used by all users is not ready for production use.
+On self-managed GitLab, by default this feature is available to all users. To make it available for administrators only,
+ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `project_owners_list_project_pending_deletion`.
+On GitLab.com, this feature is available to all users.
When delayed project deletion is [enabled for a group](../group/index.md#enable-delayed-project-deletion),
projects within that group are not deleted immediately, but only after a delay. To access a list of all projects that are pending deletion:
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 62171528695..a82c5681911 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -132,6 +132,29 @@ RSpec.describe GroupsController, factory_default: :keep do
end
end
end
+
+ describe 'require_verification_for_namespace_creation experiment', :experiment do
+ before do
+ sign_in(owner)
+ stub_experiments(require_verification_for_namespace_creation: :candidate)
+ end
+
+ it 'tracks a "start_create_group" event' do
+ expect(experiment(:require_verification_for_namespace_creation)).to track(
+ :start_create_group
+ ).on_next_instance.with_context(user: owner)
+
+ get :new
+ end
+
+ context 'when creating a sub-group' do
+ it 'does not track a "start_create_group" event' do
+ expect(experiment(:require_verification_for_namespace_creation)).not_to track(:start_create_group)
+
+ get :new, params: { parent_id: group.id }
+ end
+ end
+ end
end
describe 'GET #activity' do
diff --git a/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb
index 87417fe1637..269b6222020 100644
--- a/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb
+++ b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
subject(:experiment) { described_class.new(user: user) }
- let_it_be(:user) { create(:user) }
+ let(:user_created_at) { RequireVerificationForNamespaceCreationExperiment::EXPERIMENT_START_DATE + 1.hour }
+ let(:user) { create(:user, created_at: user_created_at) }
describe '#candidate?' do
context 'when experiment subject is candidate' do
@@ -56,4 +57,21 @@ RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
end
end
end
+
+ describe 'exclusions' do
+ context 'when user is new' do
+ it 'is not excluded' do
+ expect(subject).not_to exclude(user: user)
+ end
+ end
+
+ context 'when user is NOT new' do
+ let(:user_created_at) { RequireVerificationForNamespaceCreationExperiment::EXPERIMENT_START_DATE - 1.day }
+ let(:user) { create(:user, created_at: user_created_at) }
+
+ it 'is excluded' do
+ expect(subject).to exclude(user: user)
+ end
+ end
+ end
end
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
index 7cdf21dde46..a6f3e00fde1 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -3,6 +3,7 @@
exports[`packages_list_app renders 1`] = `
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
index b0e586f189a..72d08d5683b 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
@@ -10,7 +10,9 @@ describe('Infrastructure Title', () => {
const findTitleArea = () => wrapper.find(TitleArea);
const findMetadataItem = () => wrapper.find(MetadataItem);
- const mountComponent = (propsData = { helpUrl: 'foo' }) => {
+ const exampleProps = { helpUrl: 'http://example.gitlab.com/help' };
+
+ const mountComponent = (propsData = exampleProps) => {
wrapper = shallowMount(component, {
store,
propsData,
@@ -26,23 +28,36 @@ describe('Infrastructure Title', () => {
});
describe('title area', () => {
- it('exists', () => {
+ beforeEach(() => {
mountComponent();
+ });
+ it('exists', () => {
expect(findTitleArea().exists()).toBe(true);
});
- it('has the correct props', () => {
- mountComponent();
+ it('has the correct title', () => {
+ expect(findTitleArea().props('title')).toBe('Infrastructure Registry');
+ });
- expect(findTitleArea().props()).toMatchObject({
- title: 'Infrastructure Registry',
- infoMessages: [
+ describe('with no modules', () => {
+ it('has no info message', () => {
+ expect(findTitleArea().props('infoMessages')).toStrictEqual([]);
+ });
+ });
+
+ describe('with at least one module', () => {
+ beforeEach(() => {
+ mountComponent({ ...exampleProps, count: 1 });
+ });
+
+ it('has an info message', () => {
+ expect(findTitleArea().props('infoMessages')).toStrictEqual([
{
text: 'Publish and share your modules. %{docLinkStart}More information%{docLinkEnd}',
- link: 'foo',
+ link: exampleProps.helpUrl,
},
- ],
+ ]);
});
});
});
@@ -51,15 +66,15 @@ describe('Infrastructure Title', () => {
count | exist | text
${null} | ${false} | ${''}
${undefined} | ${false} | ${''}
- ${0} | ${true} | ${'0 Modules'}
+ ${0} | ${false} | ${''}
${1} | ${true} | ${'1 Module'}
${2} | ${true} | ${'2 Modules'}
`('when count is $count metadata item', ({ count, exist, text }) => {
beforeEach(() => {
- mountComponent({ count, helpUrl: 'foo' });
+ mountComponent({ ...exampleProps, count });
});
- it(`is ${exist} that it exists`, () => {
+ it(exist ? 'exists' : 'does not exist', () => {
expect(findMetadataItem().exists()).toBe(exist);
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
index 4c536b6d56a..31616e0b2f5 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
@@ -35,7 +35,7 @@ describe('packages_list_app', () => {
const findListComponent = () => wrapper.find(PackageList);
const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch);
- const createStore = (filter = []) => {
+ const createStore = ({ filter = [], packageCount = 0 } = {}) => {
store = new Vuex.Store({
state: {
isLoading: false,
@@ -46,6 +46,9 @@ describe('packages_list_app', () => {
packageHelpUrl: 'foo',
},
filter,
+ pagination: {
+ total: packageCount,
+ },
},
});
store.dispatch = jest.fn();
@@ -68,6 +71,7 @@ describe('packages_list_app', () => {
beforeEach(() => {
createStore();
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({});
+ mountComponent();
});
afterEach(() => {
@@ -75,30 +79,26 @@ describe('packages_list_app', () => {
});
it('renders', () => {
+ createStore({ packageCount: 1 });
mountComponent();
+
expect(wrapper.element).toMatchSnapshot();
});
- it('call requestPackagesList on page:changed', () => {
- mountComponent();
- store.dispatch.mockClear();
-
+ it('calls requestPackagesList on page:changed', () => {
const list = findListComponent();
list.vm.$emit('page:changed', 1);
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList', { page: 1 });
});
- it('call requestDeletePackage on package:delete', () => {
- mountComponent();
-
+ it('calls requestDeletePackage on package:delete', () => {
const list = findListComponent();
list.vm.$emit('package:delete', 'foo');
+
expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
});
- it('does call requestPackagesList only one time on render', () => {
- mountComponent();
-
+ it('calls requestPackagesList only once on render', () => {
expect(store.dispatch).toHaveBeenCalledTimes(3);
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', expect.any(Object));
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', expect.any(Array));
@@ -113,9 +113,12 @@ describe('packages_list_app', () => {
orderBy: 'created',
};
- it('calls setSorting with the query string based sorting', () => {
+ beforeEach(() => {
+ createStore();
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
+ });
+ it('calls setSorting with the query string based sorting', () => {
mountComponent();
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', {
@@ -125,8 +128,6 @@ describe('packages_list_app', () => {
});
it('calls setFilter with the query string based filters', () => {
- jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
-
mountComponent();
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', [
@@ -150,8 +151,6 @@ describe('packages_list_app', () => {
describe('empty state', () => {
it('generate the correct empty list link', () => {
- mountComponent();
-
const link = findListComponent().find(GlLink);
expect(link.attributes('href')).toBe(emptyListHelpUrl);
@@ -159,8 +158,6 @@ describe('packages_list_app', () => {
});
it('includes the right content on the default tab', () => {
- mountComponent();
-
const heading = findEmptyState().find('h1');
expect(heading.text()).toBe('There are no packages yet');
@@ -169,7 +166,7 @@ describe('packages_list_app', () => {
describe('filter without results', () => {
beforeEach(() => {
- createStore([{ type: 'something' }]);
+ createStore({ filter: [{ type: 'something' }] });
mountComponent();
});
@@ -181,20 +178,30 @@ describe('packages_list_app', () => {
});
});
- describe('Search', () => {
- it('exists', () => {
- mountComponent();
-
- expect(findInfrastructureSearch().exists()).toBe(true);
+ describe('search', () => {
+ describe('with no packages', () => {
+ it('does not exist', () => {
+ expect(findInfrastructureSearch().exists()).toBe(false);
+ });
});
- it('on update fetches data from the store', () => {
- mountComponent();
- store.dispatch.mockClear();
+ describe('with packages', () => {
+ beforeEach(() => {
+ createStore({ packageCount: 1 });
+ mountComponent();
+ });
- findInfrastructureSearch().vm.$emit('update');
+ it('exists', () => {
+ expect(findInfrastructureSearch().exists()).toBe(true);
+ });
- expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
+ it('on update fetches data from the store', () => {
+ store.dispatch.mockClear();
+
+ findInfrastructureSearch().vm.$emit('update');
+
+ expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
+ });
});
});
diff --git a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
index 7214225c32c..f6f4a3f6115 100644
--- a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
@@ -87,7 +87,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindin
let!(:unrelated_finding) do
create_finding!(
id: 9999999,
- uuid: "unreleated_finding",
+ uuid: Gitlab::UUID.v5(SecureRandom.hex),
vulnerability_id: nil,
report_type: 1,
location_fingerprint: 'random_location_fingerprint',
diff --git a/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb
new file mode 100644
index 00000000000..3e450546315
--- /dev/null
+++ b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddInsertOrUpdateVulnerabilityReadsTrigger do
+ let(:migration) { described_class.new }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let(:vulnerability2) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ let(:finding) do
+ create_finding!(
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'UPDATE trigger' do
+ context 'when vulnerability_id is updated' do
+ it 'creates a new vulnerability_reads row' do
+ expect do
+ finding.update!(vulnerability_id: vulnerability.id)
+ end.to change { vulnerability_reads.count }.from(0).to(1)
+ end
+ end
+
+ context 'when vulnerability_id is not updated' do
+ it 'does not create a new vulnerability_reads row' do
+ finding.update!(vulnerability_id: nil)
+
+ expect do
+ finding.update!(location: '')
+ end.not_to change { vulnerability_reads.count }
+ end
+ end
+ end
+
+ describe 'INSERT trigger' do
+ context 'when vulnerability_id is set' do
+ it 'creates a new vulnerability_reads row' do
+ expect do
+ create_finding!(
+ vulnerability_id: vulnerability2.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+ end.to change { vulnerability_reads.count }.from(0).to(1)
+ end
+ end
+
+ context 'when vulnerability_id is not set' do
+ it 'does not create a new vulnerability_reads row' do
+ expect do
+ create_finding!(
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+ end.not_to change { vulnerability_reads.count }
+ end
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ end
+
+ it 'drops the trigger' do
+ expect do
+ finding.update!(vulnerability_id: vulnerability.id)
+ end.not_to change { vulnerability_reads.count }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb
new file mode 100644
index 00000000000..d988b1e42b9
--- /dev/null
+++ b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddUpdateVulnerabilityReadsTrigger do
+ let(:migration) { described_class.new }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:issue_links) { table(:vulnerability_issue_links) }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ report_type: 7,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'UPDATE trigger' do
+ before do
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ primary_identifier_id: identifier.id
+ )
+ end
+
+ context 'when vulnerability attributes are updated' do
+ it 'updates vulnerability attributes in vulnerability_reads' do
+ expect do
+ vulnerability.update!(severity: 6)
+ end.to change { vulnerability_reads.first.severity }.from(7).to(6)
+ end
+ end
+
+ context 'when vulnerability attributes are not updated' do
+ it 'does not update vulnerability attributes in vulnerability_reads' do
+ expect do
+ vulnerability.update!(title: "New vulnerability")
+ end.not_to change { vulnerability_reads.first }
+ end
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ primary_identifier_id: identifier.id
+ )
+ end
+
+ it 'drops the trigger' do
+ expect do
+ vulnerability.update!(severity: 6)
+ end.not_to change { vulnerability_reads.first.severity }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb
new file mode 100644
index 00000000000..901f1cf6041
--- /dev/null
+++ b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddUpdateVulnerabilityReadsLocationTrigger do
+ let(:migration) { described_class.new }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:issue_links) { table(:vulnerability_issue_links) }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ report_type: 7,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'UPDATE trigger' do
+ context 'when image is updated' do
+ it 'updates location_image in vulnerability_reads' do
+ finding = create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ location: { "image" => "alpine:3.4" },
+ primary_identifier_id: identifier.id
+ )
+
+ expect do
+ finding.update!(location: { "image" => "alpine:4", "kubernetes_resource" => { "agent_id" => "1234" } })
+ end.to change { vulnerability_reads.first.location_image }.from("alpine:3.4").to("alpine:4")
+ end
+ end
+
+ context 'when image is not updated' do
+ it 'updates location_image in vulnerability_reads' do
+ finding = create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ location: { "image" => "alpine:3.4", "kubernetes_resource" => { "agent_id" => "1234" } },
+ primary_identifier_id: identifier.id
+ )
+
+ expect do
+ finding.update!(project_fingerprint: "123qweasdzx")
+ end.not_to change { vulnerability_reads.first.location_image }
+ end
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ end
+
+ it 'drops the trigger' do
+ finding = create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+
+ expect do
+ finding.update!(location: '{"image":"alpine:4"}')
+ end.not_to change { vulnerability_reads.first.location_image }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb
new file mode 100644
index 00000000000..8e50b74eb9c
--- /dev/null
+++ b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddHasIssuesOnVulnerabilityReadsTrigger do
+ let(:migration) { described_class.new }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:issue_links) { table(:vulnerability_issue_links) }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ before do
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+
+ @vulnerability_read = vulnerability_reads.first
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'INSERT trigger' do
+ it 'updates has_issues in vulnerability_reads' do
+ expect do
+ issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ end.to change { @vulnerability_read.reload.has_issues }.from(false).to(true)
+ end
+ end
+
+ describe 'DELETE trigger' do
+ let(:issue2) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+
+ it 'does not change has_issues when there exists another issue' do
+ issue_link1 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue2.id)
+
+ expect do
+ issue_link1.delete
+ end.not_to change { @vulnerability_read.reload.has_issues }
+ end
+
+ it 'unsets has_issues when all issues are deleted' do
+ issue_link1 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ issue_link2 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue2.id)
+
+ expect do
+ issue_link1.delete
+ issue_link2.delete
+ end.to change { @vulnerability_read.reload.has_issues }.from(true).to(false)
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ end
+
+ it 'drops the trigger' do
+ expect do
+ issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ end.not_to change { @vulnerability_read.has_issues }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end