From 0f50c47cd7f7b88cc61e954d601b90fe7d12aac3 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 15 Feb 2022 18:14:39 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/rails.gitlab-ci.yml | 7 + .gitlab/ci/rules.gitlab-ci.yml | 5 + .../environments/components/deployment.vue | 63 +++- .../runner/components/cells/link_cell.vue | 27 ++ .../runner/components/runner_details.vue | 21 +- .../runner/components/runner_jobs.vue | 82 +++++ .../runner/components/runner_jobs_table.vue | 95 ++++++ app/assets/javascripts/runner/constants.js | 2 + .../graphql/get_runner_jobs.query.graphql | 36 +++ .../runner_details_shared.fragment.graphql | 1 + config/gitlab_loose_foreign_keys.yml | 297 +++++++++--------- ...20_add_scan_method_to_dast_site_profile.rb | 11 + ...efault_scan_method_of_dast_site_profile.rb | 24 ++ ...roval_rules_code_owners_rule_type_index.rb | 23 ++ db/schema_migrations/20220119220620 | 1 + db/schema_migrations/20220128155814 | 1 + db/schema_migrations/20220208171826 | 1 + db/structure.sql | 1 + doc/.vale/gitlab/SubstitutionSuggestions.yml | 1 + .../documentation/styleguide/word_list.md | 2 + doc/development/testing_guide/review_apps.md | 1 + .../img/issues_created_per_month_v13_11.png | Bin 21731 -> 0 bytes .../img/issues_created_per_month_v14_8.png | Bin 0 -> 17292 bytes doc/user/analytics/issue_analytics.md | 2 +- .../img/personal_readme_setup_v14_5.png | Bin 0 -> 26192 bytes doc/user/profile/index.md | 30 +- lib/gitlab/ci/lint.rb | 33 +- lib/gitlab/ci/pipeline/logger.rb | 2 +- lib/gitlab/database.rb | 20 ++ lib/tasks/gitlab/db.rake | 13 + locale/gitlab.pot | 24 +- spec/frontend/environments/deployment_spec.js | 22 +- spec/frontend/fixtures/runner.rb | 17 + .../runner/components/cells/link_cell_spec.js | 72 +++++ .../runner/components/runner_details_spec.js | 42 ++- .../runner/components/runner_jobs_spec.js | 156 +++++++++ .../components/runner_jobs_table_spec.js | 119 +++++++ .../components/runner_update_form_spec.js | 3 +- spec/frontend/runner/mock_data.js | 2 + spec/lib/gitlab/ci/lint_spec.rb | 98 ++++++ spec/lib/gitlab/ci/pipeline/logger_spec.rb | 29 ++ .../database/loose_foreign_keys_spec.rb | 9 + spec/lib/gitlab/database_spec.rb | 28 ++ ..._rules_code_owners_rule_type_index_spec.rb | 33 ++ ...t_scan_method_of_dast_site_profile_spec.rb | 32 ++ spec/tasks/gitlab/db_rake_spec.rb | 38 +++ 46 files changed, 1341 insertions(+), 185 deletions(-) create mode 100644 app/assets/javascripts/runner/components/cells/link_cell.vue create mode 100644 app/assets/javascripts/runner/components/runner_jobs.vue create mode 100644 app/assets/javascripts/runner/components/runner_jobs_table.vue create mode 100644 app/assets/javascripts/runner/graphql/get_runner_jobs.query.graphql create mode 100644 db/migrate/20220119220620_add_scan_method_to_dast_site_profile.rb create mode 100644 db/migrate/20220208171826_update_default_scan_method_of_dast_site_profile.rb create mode 100644 db/post_migrate/20220128155814_fix_approval_rules_code_owners_rule_type_index.rb create mode 100644 db/schema_migrations/20220119220620 create mode 100644 db/schema_migrations/20220128155814 create mode 100644 db/schema_migrations/20220208171826 delete mode 100644 doc/user/analytics/img/issues_created_per_month_v13_11.png create mode 100644 doc/user/analytics/img/issues_created_per_month_v14_8.png create mode 100644 doc/user/profile/img/personal_readme_setup_v14_5.png create mode 100644 spec/frontend/runner/components/cells/link_cell_spec.js create mode 100644 spec/frontend/runner/components/runner_jobs_spec.js create mode 100644 spec/frontend/runner/components/runner_jobs_table_spec.js create mode 100644 spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb create mode 100644 spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 2ae81dbb5ba..8a4ea690c60 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -429,6 +429,13 @@ db:check-migrations-decomposed: - .decomposed-database - .rails:rules:decomposed-databases +db:migrate-non-superuser: + extends: + - .db-job-base + - .rails:rules:ee-and-foss-mr-with-migration + script: + - bundle exec rake gitlab:db:reset_as_non_superuser + db:gitlabcom-database-testing: extends: .rails:rules:db:gitlabcom-database-testing stage: test diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 2f1dbfce860..8204731e67a 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -230,6 +230,9 @@ .controllers-patterns: &controllers-patterns - "{,ee/,jh/}{app/controllers}/**/*" +.models-patterns: &models-patterns + - "{,ee/,jh/}{app/models}/**/*" + .startup-css-patterns: &startup-css-patterns - "{,ee/,jh/}app/assets/stylesheets/startup/**/*" @@ -1429,6 +1432,8 @@ changes: *frontend-patterns - <<: *if-dot-com-gitlab-org-merge-request changes: *controllers-patterns + - <<: *if-dot-com-gitlab-org-merge-request + changes: *models-patterns - <<: *if-dot-com-gitlab-org-merge-request changes: *qa-patterns - <<: *if-dot-com-gitlab-org-merge-request diff --git a/app/assets/javascripts/environments/components/deployment.vue b/app/assets/javascripts/environments/components/deployment.vue index 8c5040035d4..f98edb6bb7d 100644 --- a/app/assets/javascripts/environments/components/deployment.vue +++ b/app/assets/javascripts/environments/components/deployment.vue @@ -6,10 +6,10 @@ import { GlIcon, GlLink, GlTooltipDirective as GlTooltip, + GlTruncate, } from '@gitlab/ui'; import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import { __, s__ } from '~/locale'; -import { truncate } from '~/lib/utils/text_utility'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import DeploymentStatusBadge from './deployment_status_badge.vue'; @@ -25,6 +25,7 @@ export default { GlCollapse, GlIcon, GlLink, + GlTruncate, TimeAgoTooltip, }, directives: { @@ -75,7 +76,7 @@ export default { return this.deployment?.user; }, username() { - return truncate(this.user?.username, 25); + return `@${this.user.username}`; }, userPath() { return this.user?.path; @@ -84,11 +85,23 @@ export default { return this.deployment?.deployable; }, jobName() { - return truncate(this.deployable?.name ?? '', 25); + return this.deployable?.name; }, jobPath() { return this.deployable?.buildPath; }, + refLabel() { + return this.deployment?.tag ? this.$options.i18n.tag : this.$options.i18n.branch; + }, + ref() { + return this.deployment?.ref; + }, + refName() { + return this.ref?.name; + }, + refPath() { + return this.ref?.refPath; + }, }, methods: { toggleCollapse() { @@ -105,6 +118,8 @@ export default { triggerer: s__('Deployment|Triggerer'), job: __('Job'), api: __('API'), + branch: __('Branch'), + tag: __('Tag'), }, headerClasses: [ 'gl-display-flex', @@ -144,10 +159,12 @@ export default {
- #{{ iid }} + + #{{ iid }}
- - + + @@ -180,25 +200,40 @@ export default { -
-
- {{ $options.i18n.triggerer }} - @{{ username }} +
+
+ {{ $options.i18n.triggerer }} + + +
-
- +
+ {{ $options.i18n.job }} - {{ jobName }} + - {{ jobName }} + {{ $options.i18n.api }}
+
+ {{ refLabel }} + + + +
diff --git a/app/assets/javascripts/runner/components/cells/link_cell.vue b/app/assets/javascripts/runner/components/cells/link_cell.vue new file mode 100644 index 00000000000..2843ddbacaf --- /dev/null +++ b/app/assets/javascripts/runner/components/cells/link_cell.vue @@ -0,0 +1,27 @@ + + + diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue index ab4b99d4186..b6a5ffc7a64 100644 --- a/app/assets/javascripts/runner/components/runner_details.vue +++ b/app/assets/javascripts/runner/components/runner_details.vue @@ -1,22 +1,26 @@ + + diff --git a/app/assets/javascripts/runner/components/runner_jobs_table.vue b/app/assets/javascripts/runner/components/runner_jobs_table.vue new file mode 100644 index 00000000000..7817577bab0 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_jobs_table.vue @@ -0,0 +1,95 @@ + + + diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js index 45e61768d1e..1544efaaae2 100644 --- a/app/assets/javascripts/runner/constants.js +++ b/app/assets/javascripts/runner/constants.js @@ -4,6 +4,7 @@ export const RUNNER_PAGE_SIZE = 20; export const RUNNER_JOB_COUNT_LIMIT = 1000; export const RUNNER_DETAILS_PROJECTS_PAGE_SIZE = 5; +export const RUNNER_DETAILS_JOBS_PAGE_SIZE = 30; export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.'); export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}'); @@ -45,6 +46,7 @@ export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run export const I18N_ASSIGNED_PROJECTS = s__('Runners|Assigned Projects (%{projectCount})'); export const I18N_NONE = __('None'); +export const I18N_NO_JOBS_FOUND = s__('Runner|This runner has not run any jobs.'); // Styles diff --git a/app/assets/javascripts/runner/graphql/get_runner_jobs.query.graphql b/app/assets/javascripts/runner/graphql/get_runner_jobs.query.graphql new file mode 100644 index 00000000000..2b1decd3ddd --- /dev/null +++ b/app/assets/javascripts/runner/graphql/get_runner_jobs.query.graphql @@ -0,0 +1,36 @@ +#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" + +query getRunnerJobs($id: CiRunnerID!, $first: Int, $last: Int, $before: String, $after: String) { + runner(id: $id) { + id + projectCount + jobs(before: $before, after: $after, first: $first, last: $last) { + nodes { + id + detailedStatus { + # fields for `` + id + detailsPath + group + icon + text + } + pipeline { + id + project { + id + name + webUrl + } + } + shortSha + commitPath + tags + finishedAt + } + pageInfo { + ...PageInfo + } + } + } +} diff --git a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql index ae29fa3a4df..74760bbaa07 100644 --- a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql @@ -8,6 +8,7 @@ fragment RunnerDetailsShared on CiRunner { ipAddress description maximumTimeout + jobCount tagList createdAt status(legacyMode: null) diff --git a/config/gitlab_loose_foreign_keys.yml b/config/gitlab_loose_foreign_keys.yml index 154e563ad99..f46e005dacf 100644 --- a/config/gitlab_loose_foreign_keys.yml +++ b/config/gitlab_loose_foreign_keys.yml @@ -1,37 +1,36 @@ +# Make sure that this file has the keys sorted --- -dast_site_profiles_pipelines: - - table: ci_pipelines - column: ci_pipeline_id - on_delete: async_delete -vulnerability_feedback: - - table: ci_pipelines - column: pipeline_id - on_delete: async_nullify -ci_pipeline_chat_data: - - table: chat_names - column: chat_name_id - on_delete: async_delete -dast_scanner_profiles_builds: - - table: ci_builds - column: ci_build_id - on_delete: async_delete -dast_site_profiles_builds: - - table: ci_builds - column: ci_build_id - on_delete: async_delete -dast_profiles_pipelines: - - table: ci_pipelines - column: ci_pipeline_id - on_delete: async_delete -clusters_applications_runners: - - table: ci_runners - column: runner_id - on_delete: async_nullify -ci_variables: +ci_build_report_results: - table: projects column: project_id on_delete: async_delete -ci_runner_projects: +ci_builds: + - table: users + column: user_id + on_delete: async_nullify + - table: projects + column: project_id + on_delete: async_delete +ci_builds_metadata: + - table: projects + column: project_id + on_delete: async_delete +ci_daily_build_group_report_results: + - table: namespaces + column: group_id + on_delete: async_delete + - table: projects + column: project_id + on_delete: async_delete +ci_freeze_periods: + - table: projects + column: project_id + on_delete: async_delete +ci_group_variables: + - table: namespaces + column: group_id + on_delete: async_delete +ci_job_artifacts: - table: projects column: project_id on_delete: async_delete @@ -45,20 +44,13 @@ ci_job_token_project_scope_links: - table: projects column: target_project_id on_delete: async_delete -ci_daily_build_group_report_results: +ci_minutes_additional_packs: - table: namespaces - column: group_id + column: namespace_id on_delete: async_delete - - table: projects - column: project_id - on_delete: async_delete -external_pull_requests: - - table: projects - column: project_id - on_delete: async_delete -ci_freeze_periods: - - table: projects - column: project_id +ci_namespace_mirrors: + - table: namespaces + column: namespace_id on_delete: async_delete ci_pending_builds: - table: namespaces @@ -67,37 +59,17 @@ ci_pending_builds: - table: projects column: project_id on_delete: async_delete -ci_resource_groups: +ci_pipeline_artifacts: - table: projects column: project_id on_delete: async_delete -ci_runner_namespaces: - - table: namespaces - column: namespace_id +ci_pipeline_chat_data: + - table: chat_names + column: chat_name_id on_delete: async_delete -ci_running_builds: - - table: projects - column: project_id - on_delete: async_delete -ci_namespace_mirrors: - - table: namespaces - column: namespace_id - on_delete: async_delete -ci_sources_projects: - - table: projects - column: source_project_id - on_delete: async_delete -ci_build_report_results: - - table: projects - column: project_id - on_delete: async_delete -ci_job_artifacts: - - table: projects - column: project_id - on_delete: async_delete -ci_builds: +ci_pipeline_schedules: - table: users - column: user_id + column: owner_id on_delete: async_nullify - table: projects column: project_id @@ -122,97 +94,31 @@ ci_project_mirrors: - table: namespaces column: namespace_id on_delete: async_delete -ci_unit_tests: - - table: projects - column: project_id - on_delete: async_delete -merge_requests: - - table: ci_pipelines - column: head_pipeline_id - on_delete: async_nullify -vulnerability_statistics: - - table: ci_pipelines - column: latest_pipeline_id - on_delete: async_nullify -vulnerability_occurrence_pipelines: - - table: ci_pipelines - column: pipeline_id - on_delete: async_delete -packages_build_infos: - - table: ci_pipelines - column: pipeline_id - on_delete: async_nullify -packages_package_file_build_infos: - - table: ci_pipelines - column: pipeline_id - on_delete: async_nullify ci_project_monthly_usages: - table: projects column: project_id on_delete: async_delete -pages_deployments: - - table: ci_builds - column: ci_build_id - on_delete: async_nullify -ci_builds_metadata: - - table: projects - column: project_id - on_delete: async_delete -terraform_state_versions: - - table: ci_builds - column: ci_build_id - on_delete: async_nullify -merge_request_metrics: - - table: ci_pipelines - column: pipeline_id - on_delete: async_delete -project_pages_metadata: - - table: ci_job_artifacts - column: artifacts_archive_id - on_delete: async_nullify -ci_pipeline_schedules: - - table: users - column: owner_id - on_delete: async_nullify - - table: projects - column: project_id - on_delete: async_delete -merge_trains: - - table: ci_pipelines - column: pipeline_id - on_delete: async_nullify ci_refs: - table: projects column: project_id on_delete: async_delete -ci_group_variables: - - table: namespaces - column: group_id - on_delete: async_delete -ci_minutes_additional_packs: - - table: namespaces - column: namespace_id - on_delete: async_delete -requirements_management_test_reports: - - table: ci_builds - column: build_id - on_delete: async_nullify -ci_subscriptions_projects: - - table: projects - column: downstream_project_id - on_delete: async_delete - - table: projects - column: upstream_project_id - on_delete: async_delete -security_scans: - - table: ci_builds - column: build_id - on_delete: async_delete -ci_secure_files: +ci_resource_groups: - table: projects column: project_id on_delete: async_delete -ci_pipeline_artifacts: +ci_runner_namespaces: + - table: namespaces + column: namespace_id + on_delete: async_delete +ci_runner_projects: + - table: projects + column: project_id + on_delete: async_delete +ci_running_builds: + - table: projects + column: project_id + on_delete: async_delete +ci_secure_files: - table: projects column: project_id on_delete: async_delete @@ -223,10 +129,21 @@ ci_sources_pipelines: - table: projects column: project_id on_delete: async_delete +ci_sources_projects: + - table: projects + column: source_project_id + on_delete: async_delete ci_stages: - table: projects column: project_id on_delete: async_delete +ci_subscriptions_projects: + - table: projects + column: downstream_project_id + on_delete: async_delete + - table: projects + column: upstream_project_id + on_delete: async_delete ci_triggers: - table: users column: owner_id @@ -234,3 +151,87 @@ ci_triggers: - table: projects column: project_id on_delete: async_delete +ci_unit_tests: + - table: projects + column: project_id + on_delete: async_delete +ci_variables: + - table: projects + column: project_id + on_delete: async_delete +clusters_applications_runners: + - table: ci_runners + column: runner_id + on_delete: async_nullify +dast_profiles_pipelines: + - table: ci_pipelines + column: ci_pipeline_id + on_delete: async_delete +dast_scanner_profiles_builds: + - table: ci_builds + column: ci_build_id + on_delete: async_delete +dast_site_profiles_builds: + - table: ci_builds + column: ci_build_id + on_delete: async_delete +dast_site_profiles_pipelines: + - table: ci_pipelines + column: ci_pipeline_id + on_delete: async_delete +external_pull_requests: + - table: projects + column: project_id + on_delete: async_delete +merge_request_metrics: + - table: ci_pipelines + column: pipeline_id + on_delete: async_delete +merge_requests: + - table: ci_pipelines + column: head_pipeline_id + on_delete: async_nullify +merge_trains: + - table: ci_pipelines + column: pipeline_id + on_delete: async_nullify +packages_build_infos: + - table: ci_pipelines + column: pipeline_id + on_delete: async_nullify +packages_package_file_build_infos: + - table: ci_pipelines + column: pipeline_id + on_delete: async_nullify +pages_deployments: + - table: ci_builds + column: ci_build_id + on_delete: async_nullify +project_pages_metadata: + - table: ci_job_artifacts + column: artifacts_archive_id + on_delete: async_nullify +requirements_management_test_reports: + - table: ci_builds + column: build_id + on_delete: async_nullify +security_scans: + - table: ci_builds + column: build_id + on_delete: async_delete +terraform_state_versions: + - table: ci_builds + column: ci_build_id + on_delete: async_nullify +vulnerability_feedback: + - table: ci_pipelines + column: pipeline_id + on_delete: async_nullify +vulnerability_occurrence_pipelines: + - table: ci_pipelines + column: pipeline_id + on_delete: async_delete +vulnerability_statistics: + - table: ci_pipelines + column: latest_pipeline_id + on_delete: async_nullify diff --git a/db/migrate/20220119220620_add_scan_method_to_dast_site_profile.rb b/db/migrate/20220119220620_add_scan_method_to_dast_site_profile.rb new file mode 100644 index 00000000000..f7b7580d673 --- /dev/null +++ b/db/migrate/20220119220620_add_scan_method_to_dast_site_profile.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddScanMethodToDastSiteProfile < Gitlab::Database::Migration[1.0] + def up + add_column :dast_site_profiles, :scan_method, :integer, limit: 2, default: 0, null: false + end + + def down + remove_column :dast_site_profiles, :scan_method + end +end diff --git a/db/migrate/20220208171826_update_default_scan_method_of_dast_site_profile.rb b/db/migrate/20220208171826_update_default_scan_method_of_dast_site_profile.rb new file mode 100644 index 00000000000..b01dbe642e2 --- /dev/null +++ b/db/migrate/20220208171826_update_default_scan_method_of_dast_site_profile.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# See https://docs.gitlab.com/ee/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class UpdateDefaultScanMethodOfDastSiteProfile < Gitlab::Database::Migration[1.0] + BATCH_SIZE = 500 + + disable_ddl_transaction! + + def up + each_batch_range('dast_site_profiles', scope: ->(table) { table.where(target_type: 1) }, of: BATCH_SIZE) do |min, max| + execute <<~SQL + UPDATE dast_site_profiles + SET scan_method = 1 + WHERE id BETWEEN #{min} AND #{max} + SQL + end + end + + def down + # noop + end +end diff --git a/db/post_migrate/20220128155814_fix_approval_rules_code_owners_rule_type_index.rb b/db/post_migrate/20220128155814_fix_approval_rules_code_owners_rule_type_index.rb new file mode 100644 index 00000000000..eccfab25126 --- /dev/null +++ b/db/post_migrate/20220128155814_fix_approval_rules_code_owners_rule_type_index.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class FixApprovalRulesCodeOwnersRuleTypeIndex < Gitlab::Database::Migration[1.0] + INDEX_NAME = 'index_approval_rules_code_owners_rule_type' + OLD_INDEX_NAME = 'index_approval_rules_code_owners_rule_type_old' + TABLE = :approval_merge_request_rules + COLUMN = :merge_request_id + WHERE_CONDITION = 'rule_type = 2' + + disable_ddl_transaction! + + def up + rename_index TABLE, INDEX_NAME, OLD_INDEX_NAME if index_exists_by_name?(TABLE, INDEX_NAME) && !index_exists_by_name?(TABLE, OLD_INDEX_NAME) + + add_concurrent_index TABLE, COLUMN, where: WHERE_CONDITION, name: INDEX_NAME + + remove_concurrent_index_by_name TABLE, OLD_INDEX_NAME + end + + def down + # No-op + end +end diff --git a/db/schema_migrations/20220119220620 b/db/schema_migrations/20220119220620 new file mode 100644 index 00000000000..a6a9abb2acc --- /dev/null +++ b/db/schema_migrations/20220119220620 @@ -0,0 +1 @@ +535f476a358dcb3f3472f1e0ec1afef738f995197b5d1f4fcd61e58a9c9e8e75 \ No newline at end of file diff --git a/db/schema_migrations/20220128155814 b/db/schema_migrations/20220128155814 new file mode 100644 index 00000000000..209b0874a84 --- /dev/null +++ b/db/schema_migrations/20220128155814 @@ -0,0 +1 @@ +77cc8fc86f2c6a5ed017dde40dd4db796821a35e6ce4d8dcbe24b2cdaccbb5d9 \ No newline at end of file diff --git a/db/schema_migrations/20220208171826 b/db/schema_migrations/20220208171826 new file mode 100644 index 00000000000..75ae0dcef61 --- /dev/null +++ b/db/schema_migrations/20220208171826 @@ -0,0 +1 @@ +e48473172d7561fb7474e16e291e555843c0ec4543300b007f86cd4a5923db85 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 6460a376664..4cdfa3594bf 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13414,6 +13414,7 @@ CREATE TABLE dast_site_profiles ( auth_password_field text, auth_username text, target_type smallint DEFAULT 0 NOT NULL, + scan_method smallint DEFAULT 0 NOT NULL, CONSTRAINT check_5203110fee CHECK ((char_length(auth_username_field) <= 255)), CONSTRAINT check_6cfab17b48 CHECK ((char_length(name) <= 255)), CONSTRAINT check_c329dffdba CHECK ((char_length(auth_password_field) <= 255)), diff --git a/doc/.vale/gitlab/SubstitutionSuggestions.yml b/doc/.vale/gitlab/SubstitutionSuggestions.yml index e7c0cc04244..df132d89637 100644 --- a/doc/.vale/gitlab/SubstitutionSuggestions.yml +++ b/doc/.vale/gitlab/SubstitutionSuggestions.yml @@ -17,6 +17,7 @@ swap: e-mail: '"email"' GFM: '"GitLab Flavored Markdown"' it is recommended: '"we recommend"' + navigate: go OAuth2: '"OAuth 2.0"' once that: '"after that"' once the: '"after the"' diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index 2dc040662b7..2c435cdc69d 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -561,6 +561,8 @@ Do not use **navigate**. Use **go** instead. For example: - Go to this webpage. - Open a terminal and go to the `runner` directory. +([Vale](../testing.md#vale) rule: [`SubstitutionSuggestions.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/SubstitutionSuggestions.yml)) + ## need to, should Try to avoid **needs to**, because it's wordy. Avoid **should** when you can be more specific. If something is required, use **must**. diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index a674c08dc9e..27d5ae70ed7 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -15,6 +15,7 @@ For any of the following scenarios, the `start-review-app-pipeline` job would be - for merge requests with CI config changes - for merge requests with frontend changes - for merge requests with changes to `{,ee/,jh/}{app/controllers}/**/*` +- for merge requests with changes to `{,ee/,jh/}{app/models}/**/*` - for merge requests with QA changes - for scheduled pipelines - the MR has the `pipeline:run-review-app` label set diff --git a/doc/user/analytics/img/issues_created_per_month_v13_11.png b/doc/user/analytics/img/issues_created_per_month_v13_11.png deleted file mode 100644 index da3340bfdc22a5b499acf5ba50dce0d8b0942fb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21731 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 literal 0 HcmV?d00001 diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index cb009a8ec67..f201e04183c 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -102,20 +102,36 @@ user profiles are only visible to signed-in users. ## Add details to your profile with a README -### *Add personal README to profile* - > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232157) in GitLab 14.5. -If you want to add more information to your profile page, you can create a README file. When you populate the README file with information, it's included on your profile page. +You can add more information to your profile page with a README file. When you populate +the README file with information, it's included on your profile page. -To add a README to your profile: +### From a new project -1. Create a new public project with the same project path as your GitLab username. +To create a new project and add its README to your profile: + +1. On the top bar, select **Menu > Project**. +1. Select **Create new project**. +1. Select **Create blank project**. +1. Enter the project details: + - In the **Project name** field, enter the name for your new project. + - In the **Project URL** field, select your GitLab username. + - In the **Project slug** field, enter your GitLab username. +1. For **Visibility Level**, select **Public**. + ![Proper project path for an individual on the hosted product](img/personal_readme_setup_v14_5.png) +1. For **Project Configuration**, ensure **Initialize repository with a README** is selected. +1. Select **Create project**. 1. Create a README file inside this project. The file can be any valid [README or index file](../project/repository/index.md#readme-and-index-files). 1. Populate the README file with [Markdown](../markdown.md). -To use an existing project, [update the path](../project/settings/index.md#renaming-a-repository) of the project to match -your username. +GitLab displays the contents of your README below your contribution graph. + +### From an existing project + +To add the README from an existing project to your profile, +[update the path](../project/settings/index.md#renaming-a-repository) of the project +to match your username. ## Add external accounts to your user profile page diff --git a/lib/gitlab/ci/lint.rb b/lib/gitlab/ci/lint.rb index c93dfe3f202..5591ed62436 100644 --- a/lib/gitlab/ci/lint.rb +++ b/lib/gitlab/ci/lint.rb @@ -18,6 +18,8 @@ module Gitlab end end + LOG_MAX_DURATION_THRESHOLD = 2.seconds + def initialize(project:, current_user:, sha: nil) @project = project @current_user = current_user @@ -49,12 +51,9 @@ module Gitlab end def static_validation(content) - result = Gitlab::Ci::YamlProcessor.new( - content, - project: @project, - user: @current_user, - sha: @sha - ).execute + logger = build_logger + + result = yaml_processor_result(content, logger) Result.new( jobs: static_validation_convert_to_jobs(result), @@ -62,6 +61,17 @@ module Gitlab errors: result.errors, warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord ) + ensure + logger.commit(pipeline: ::Ci::Pipeline.new, caller: self.class.name) + end + + def yaml_processor_result(content, logger) + logger.instrument(:yaml_process) do + Gitlab::Ci::YamlProcessor.new(content, project: @project, + user: @current_user, + sha: @sha, + logger: logger).execute + end end def dry_run_convert_to_jobs(stages) @@ -109,6 +119,17 @@ module Gitlab jobs end + + def build_logger + Gitlab::Ci::Pipeline::Logger.new(project: @project) do |l| + l.log_when do |observations| + values = observations['yaml_process_duration_s'] + next false if values.empty? + + values.max >= LOG_MAX_DURATION_THRESHOLD + end + end + end end end end diff --git a/lib/gitlab/ci/pipeline/logger.rb b/lib/gitlab/ci/pipeline/logger.rb index fbba12c11a9..10c0fe295f8 100644 --- a/lib/gitlab/ci/pipeline/logger.rb +++ b/lib/gitlab/ci/pipeline/logger.rb @@ -59,7 +59,7 @@ module Gitlab attributes = { class: self.class.name.to_s, pipeline_creation_caller: caller, - project_id: project.id, + project_id: project&.id, # project is not available when called from `/ci/lint` pipeline_persisted: pipeline.persisted?, pipeline_source: pipeline.source, pipeline_creation_service_duration_s: age diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index f9c346a272f..9b32d285ec0 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -109,6 +109,26 @@ module Gitlab name.to_s == CI_DATABASE_NAME end + class PgUser < ApplicationRecord + self.table_name = 'pg_user' + self.primary_key = :usename + end + + # rubocop: disable CodeReuse/ActiveRecord + def self.check_for_non_superuser + user = PgUser.find_by('usename = CURRENT_USER') + am_i_superuser = user.usesuper + + Gitlab::AppLogger.info( + "Account details: User: \"#{user.usename}\", UseSuper: (#{am_i_superuser})" + ) + + raise 'Error: detected superuser' if am_i_superuser + rescue ActiveRecord::StatementInvalid + raise 'User CURRENT_USER not found' + end + # rubocop: enable CodeReuse/ActiveRecord + def self.check_postgres_version_and_print_warning return if Gitlab::Runtime.rails_runner? diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 64277650d42..6d4af9d166f 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -270,6 +270,19 @@ namespace :gitlab do end end + desc 'Run migration as gitlab non-superuser' + task :reset_as_non_superuser, [:username] => :environment do |_, args| + username = args.fetch(:username, 'gitlab') + puts "Migrate using username #{username}" + Rake::Task['db:drop'].invoke + Rake::Task['db:create'].invoke + ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.configuration_hash.merge(username: username)) # rubocop: disable Database/EstablishConnection + Gitlab::Database.check_for_non_superuser + Rake::Task['db:migrate'].invoke + end + end + # Only for development environments, # we execute pending data migrations inline for convenience. Rake::Task['db:migrate'].enhance do diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 76a86981cc8..c2495a600d1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -20374,6 +20374,9 @@ msgstr "" msgid "It looks like you have some draft commits in this branch." msgstr "" +msgid "It looks like you're attempting to activate your subscription. Use %{a_start}the Subscription page%{a_end} instead." +msgstr "" + msgid "It may be several days before you see feature usage data." msgstr "" @@ -20878,6 +20881,9 @@ msgstr "" msgid "Job|Erase job log and artifacts" msgstr "" +msgid "Job|Finished at" +msgstr "" + msgid "Job|Job artifacts" msgstr "" @@ -20902,6 +20908,9 @@ msgstr "" msgid "Job|Show complete raw" msgstr "" +msgid "Job|Status" +msgstr "" + msgid "Job|The artifacts were removed" msgstr "" @@ -27138,9 +27147,6 @@ msgstr "" msgid "Please enter a valid time interval" msgstr "" -msgid "Please enter or upload a valid license." -msgstr "" - msgid "Please enter your current password." msgstr "" @@ -31321,6 +31327,9 @@ msgstr "" msgid "Runners|Instance" msgstr "" +msgid "Runners|Jobs" +msgstr "" + msgid "Runners|Last contact" msgstr "" @@ -31561,6 +31570,9 @@ msgstr "" msgid "Runners|stale" msgstr "" +msgid "Runner|This runner has not run any jobs." +msgstr "" + msgid "Running" msgstr "" @@ -36336,6 +36348,9 @@ msgstr "" msgid "The latest pipeline for this merge request has failed." msgstr "" +msgid "The license key is invalid." +msgstr "" + msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc." msgstr "" @@ -36351,6 +36366,9 @@ msgstr "" msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below." msgstr "" +msgid "The license you uploaded is invalid. If the issue persists, contact support at %{link}." +msgstr "" + msgid "The list creation wizard is already open" msgstr "" diff --git a/spec/frontend/environments/deployment_spec.js b/spec/frontend/environments/deployment_spec.js index 8936eb8fd64..6cc363e000b 100644 --- a/spec/frontend/environments/deployment_spec.js +++ b/spec/frontend/environments/deployment_spec.js @@ -213,8 +213,28 @@ describe('~/environments/components/deployment.vue', () => { expect(job.attributes('href')).toBe(deployment.deployable.buildPath); const apiBadge = wrapper.findByText(__('API')); expect(apiBadge.exists()).toBe(false); + + const branchLabel = wrapper.findByText(__('Branch')); + expect(branchLabel.exists()).toBe(true); + const tagLabel = wrapper.findByText(__('Tag')); + expect(tagLabel.exists()).toBe(false); + const ref = wrapper.findByRole('link', { name: deployment.ref.name }); + expect(ref.attributes('href')).toBe(deployment.ref.refPath); }); }); + + describe('with tagged deployment', () => { + beforeEach(async () => { + wrapper = createWrapper({ propsData: { deployment: { ...deployment, tag: true } } }); + await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click'); + }); + + it('shows tag instead of branch', () => { + const refLabel = wrapper.findByText(__('Tag')); + expect(refLabel.exists()).toBe(true); + }); + }); + describe('with API deployment', () => { beforeEach(async () => { wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: null } } }); @@ -237,7 +257,7 @@ describe('~/environments/components/deployment.vue', () => { }); it('shows a span instead of a link', () => { - const job = wrapper.findByText(deployment.deployable.name); + const job = wrapper.findByTitle(deployment.deployable.name); expect(job.attributes('href')).toBeUndefined(); }); }); diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb index befb4e23b22..cdb4c3fd8ba 100644 --- a/spec/frontend/fixtures/runner.rb +++ b/spec/frontend/fixtures/runner.rb @@ -17,6 +17,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') } let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') } let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') } + let_it_be(:build) { create(:ci_build, runner: instance_runner) } query_path = 'runner/graphql/' fixtures_path = 'graphql/runner/' @@ -104,6 +105,22 @@ RSpec.describe 'Runner (JavaScript fixtures)' do expect_graphql_errors_to_be_empty end end + + describe GraphQL::Query, type: :request do + get_runner_jobs_query_name = 'get_runner_jobs.query.graphql' + + let_it_be(:query) do + get_graphql_query_as_string("#{query_path}#{get_runner_jobs_query_name}") + end + + it "#{fixtures_path}#{get_runner_jobs_query_name}.json" do + post_graphql(query, current_user: admin, variables: { + id: instance_runner.to_global_id.to_s + }) + + expect_graphql_errors_to_be_empty + end + end end describe do diff --git a/spec/frontend/runner/components/cells/link_cell_spec.js b/spec/frontend/runner/components/cells/link_cell_spec.js new file mode 100644 index 00000000000..a59a0eaa5d8 --- /dev/null +++ b/spec/frontend/runner/components/cells/link_cell_spec.js @@ -0,0 +1,72 @@ +import { GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import LinkCell from '~/runner/components/cells/link_cell.vue'; + +describe('LinkCell', () => { + let wrapper; + + const findGlLink = () => wrapper.find(GlLink); + const findSpan = () => wrapper.find('span'); + + const createComponent = ({ props = {}, ...options } = {}) => { + wrapper = shallowMountExtended(LinkCell, { + propsData: { + ...props, + }, + ...options, + }); + }; + + it('when an href is provided, renders a link', () => { + createComponent({ props: { href: '/url' } }); + expect(findGlLink().exists()).toBe(true); + }); + + it('when an href is not provided, renders no link', () => { + createComponent(); + expect(findGlLink().exists()).toBe(false); + }); + + describe.each` + href | findContent + ${null} | ${findSpan} + ${'/url'} | ${findGlLink} + `('When href is $href', ({ href, findContent }) => { + const content = 'My Text'; + const attrs = { foo: 'bar' }; + const listeners = { + click: jest.fn(), + }; + + beforeEach(() => { + createComponent({ + props: { href }, + slots: { + default: content, + }, + attrs, + listeners, + }); + }); + + afterAll(() => { + listeners.click.mockReset(); + }); + + it('Renders content', () => { + expect(findContent().text()).toBe(content); + }); + + it('Passes attributes', () => { + expect(findContent().attributes()).toMatchObject(attrs); + }); + + it('Passes event listeners', () => { + expect(listeners.click).toHaveBeenCalledTimes(0); + + findContent().vm.$emit('click'); + + expect(listeners.click).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js index dbc96a30750..6bf4a52a799 100644 --- a/spec/frontend/runner/components/runner_details_spec.js +++ b/spec/frontend/runner/components/runner_details_spec.js @@ -1,4 +1,4 @@ -import { GlSprintf, GlIntersperse } from '@gitlab/ui'; +import { GlSprintf, GlIntersperse, GlTab } from '@gitlab/ui'; import { createWrapper, ErrorWrapper } from '@vue/test-utils'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -8,6 +8,7 @@ import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner import RunnerDetails from '~/runner/components/runner_details.vue'; import RunnerDetail from '~/runner/components/runner_detail.vue'; import RunnerGroups from '~/runner/components/runner_groups.vue'; +import RunnersJobs from '~/runner/components/runner_jobs.vue'; import RunnerTags from '~/runner/components/runner_tags.vue'; import RunnerTag from '~/runner/components/runner_tag.vue'; @@ -38,6 +39,8 @@ describe('RunnerDetails', () => { }; const findDetailGroups = () => wrapper.findComponent(RunnerGroups); + const findRunnersJobs = () => wrapper.findComponent(RunnersJobs); + const findJobCountBadge = () => wrapper.findByTestId('job-count-badge'); const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => { wrapper = mountFn(RunnerDetails, { @@ -146,4 +149,41 @@ describe('RunnerDetails', () => { }); }); }); + + describe('Jobs tab', () => { + const stubs = { GlTab }; + + it('without a runner, shows no jobs', () => { + createComponent({ + props: { runner: null }, + stubs, + }); + + expect(findJobCountBadge().exists()).toBe(false); + expect(findRunnersJobs().exists()).toBe(false); + }); + + it('without a job count, shows no jobs count', () => { + createComponent({ + props: { + runner: { ...mockRunner, jobCount: undefined }, + }, + stubs, + }); + + expect(findJobCountBadge().exists()).toBe(false); + }); + + it('with a job count, shows jobs count', () => { + const runner = { ...mockRunner, jobCount: 3 }; + + createComponent({ + props: { runner }, + stubs, + }); + + expect(findJobCountBadge().text()).toBe('3'); + expect(findRunnersJobs().props('runner')).toBe(runner); + }); + }); }); diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js new file mode 100644 index 00000000000..97339056370 --- /dev/null +++ b/spec/frontend/runner/components/runner_jobs_spec.js @@ -0,0 +1,156 @@ +import { GlSkeletonLoading } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/flash'; +import RunnerJobs from '~/runner/components/runner_jobs.vue'; +import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue'; +import RunnerPagination from '~/runner/components/runner_pagination.vue'; +import { captureException } from '~/runner/sentry_utils'; +import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/runner/constants'; + +import getRunnerJobsQuery from '~/runner/graphql/get_runner_jobs.query.graphql'; + +import { runnerData, runnerJobsData } from '../mock_data'; + +jest.mock('~/flash'); +jest.mock('~/runner/sentry_utils'); + +const mockRunner = runnerData.data.runner; +const mockRunnerWithJobs = runnerJobsData.data.runner; +const mockJobs = mockRunnerWithJobs.jobs.nodes; + +Vue.use(VueApollo); + +describe('RunnerJobs', () => { + let wrapper; + let mockRunnerJobsQuery; + + const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoading); + const findRunnerJobsTable = () => wrapper.findComponent(RunnerJobsTable); + const findRunnerPagination = () => wrapper.findComponent(RunnerPagination); + + const createComponent = ({ mountFn = shallowMountExtended } = {}) => { + wrapper = mountFn(RunnerJobs, { + apolloProvider: createMockApollo([[getRunnerJobsQuery, mockRunnerJobsQuery]]), + propsData: { + runner: mockRunner, + }, + }); + }; + + beforeEach(() => { + mockRunnerJobsQuery = jest.fn(); + }); + + afterEach(() => { + mockRunnerJobsQuery.mockReset(); + wrapper.destroy(); + }); + + it('Requests runner jobs', async () => { + createComponent(); + + await waitForPromises(); + + expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(1); + expect(mockRunnerJobsQuery).toHaveBeenCalledWith({ + id: mockRunner.id, + first: RUNNER_DETAILS_JOBS_PAGE_SIZE, + }); + }); + + describe('When there are jobs assigned', () => { + beforeEach(async () => { + mockRunnerJobsQuery.mockResolvedValueOnce(runnerJobsData); + + createComponent(); + await waitForPromises(); + }); + + it('Shows jobs', () => { + const jobs = findRunnerJobsTable().props('jobs'); + + expect(jobs).toHaveLength(mockJobs.length); + expect(jobs[0]).toMatchObject(mockJobs[0]); + }); + + describe('When "Next" page is clicked', () => { + beforeEach(async () => { + findRunnerPagination().vm.$emit('input', { page: 2, after: 'AFTER_CURSOR' }); + + await waitForPromises(); + }); + + it('A new page is requested', () => { + expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(2); + expect(mockRunnerJobsQuery).toHaveBeenLastCalledWith({ + id: mockRunner.id, + first: RUNNER_DETAILS_JOBS_PAGE_SIZE, + after: 'AFTER_CURSOR', + }); + }); + }); + }); + + describe('When loading', () => { + it('shows loading indicator and no other content', () => { + createComponent(); + + expect(findGlSkeletonLoading().exists()).toBe(true); + expect(findRunnerJobsTable().exists()).toBe(false); + expect(findRunnerPagination().attributes('disabled')).toBe('true'); + }); + }); + + describe('When there are no jobs', () => { + beforeEach(async () => { + mockRunnerJobsQuery.mockResolvedValueOnce({ + data: { + runner: { + id: mockRunner.id, + projectCount: 0, + jobs: { + nodes: [], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', + }, + }, + }, + }, + }); + + createComponent(); + await waitForPromises(); + }); + + it('Shows a "None" label', () => { + expect(wrapper.text()).toBe(I18N_NO_JOBS_FOUND); + }); + }); + + describe('When an error occurs', () => { + beforeEach(async () => { + mockRunnerJobsQuery.mockRejectedValue(new Error('Error!')); + + createComponent(); + await waitForPromises(); + }); + + it('shows an error', () => { + expect(createAlert).toHaveBeenCalled(); + }); + + it('reports an error', () => { + expect(captureException).toHaveBeenCalledWith({ + component: 'RunnerJobs', + error: expect.any(Error), + }); + }); + }); +}); diff --git a/spec/frontend/runner/components/runner_jobs_table_spec.js b/spec/frontend/runner/components/runner_jobs_table_spec.js new file mode 100644 index 00000000000..5f4905ad2a8 --- /dev/null +++ b/spec/frontend/runner/components/runner_jobs_table_spec.js @@ -0,0 +1,119 @@ +import { GlTableLite } from '@gitlab/ui'; +import { + extendedWrapper, + shallowMountExtended, + mountExtended, +} from 'helpers/vue_test_utils_helper'; +import { __, s__ } from '~/locale'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue'; +import { useFakeDate } from 'helpers/fake_date'; +import { runnerJobsData } from '../mock_data'; + +const mockJobs = runnerJobsData.data.runner.jobs.nodes; + +describe('RunnerJobsTable', () => { + let wrapper; + const mockNow = '2021-01-15T12:00:00Z'; + const mockOneHourAgo = '2021-01-15T11:00:00Z'; + + useFakeDate(mockNow); + + const findTable = () => wrapper.findComponent(GlTableLite); + const findHeaders = () => wrapper.findAll('th'); + const findRows = () => wrapper.findAll('[data-testid^="job-row-"]'); + const findCell = ({ field }) => + extendedWrapper(findRows().at(0).find(`[data-testid="td-${field}"]`)); + + const createComponent = ({ props = {} } = {}, mountFn = shallowMountExtended) => { + wrapper = mountFn(RunnerJobsTable, { + propsData: { + jobs: mockJobs, + ...props, + }, + stubs: { + GlTableLite, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('Sets job id as a row key', () => { + createComponent(); + + expect(findTable().attributes('primarykey')).toBe('id'); + }); + + describe('Table data', () => { + beforeEach(() => { + createComponent({}, mountExtended); + }); + + it('Displays headers', () => { + const headerLabels = findHeaders().wrappers.map((w) => w.text()); + + expect(headerLabels).toEqual([ + s__('Job|Status'), + __('Job'), + __('Project'), + __('Commit'), + s__('Job|Finished at'), + s__('Runners|Tags'), + ]); + }); + + it('Displays a list of jobs', () => { + expect(findRows()).toHaveLength(1); + }); + + it('Displays details of a job', () => { + const { id, detailedStatus, pipeline, shortSha, commitPath } = mockJobs[0]; + + expect(findCell({ field: 'status' }).text()).toMatchInterpolatedText(detailedStatus.text); + + expect(findCell({ field: 'job' }).text()).toContain(`#${getIdFromGraphQLId(id)}`); + expect(findCell({ field: 'job' }).find('a').attributes('href')).toBe( + detailedStatus.detailsPath, + ); + + expect(findCell({ field: 'project' }).text()).toBe(pipeline.project.name); + expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe( + pipeline.project.webUrl, + ); + + expect(findCell({ field: 'commit' }).text()).toBe(shortSha); + expect(findCell({ field: 'commit' }).find('a').attributes('href')).toBe(commitPath); + }); + }); + + describe('Table data formatting', () => { + let mockJobsCopy; + + beforeEach(() => { + mockJobsCopy = [ + { + ...mockJobs[0], + }, + ]; + }); + + it('Formats finishedAt time', () => { + mockJobsCopy[0].finishedAt = mockOneHourAgo; + + createComponent({ props: { jobs: mockJobsCopy } }, mountExtended); + + expect(findCell({ field: 'finished_at' }).text()).toBe('1 hour ago'); + }); + + it('Formats tags', () => { + mockJobsCopy[0].tags = ['tag-1', 'tag-2']; + + createComponent({ props: { jobs: mockJobsCopy } }, mountExtended); + + expect(findCell({ field: 'tags' }).text()).toMatchInterpolatedText('tag-1 tag-2'); + }); + }); +}); diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js index 36120a4c7ed..8b76be396ef 100644 --- a/spec/frontend/runner/components/runner_update_form_spec.js +++ b/spec/frontend/runner/components/runner_update_form_spec.js @@ -123,6 +123,7 @@ describe('RunnerUpdateForm', () => { // Some read-only fields are not submitted const { + __typename, ipAddress, runnerType, createdAt, @@ -132,7 +133,7 @@ describe('RunnerUpdateForm', () => { userPermissions, version, groups, - __typename, + jobCount, ...submitted } = mockRunner; diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js index 7260f0fbc9a..d80caa47752 100644 --- a/spec/frontend/runner/mock_data.js +++ b/spec/frontend/runner/mock_data.js @@ -7,6 +7,7 @@ import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json'; import runnerWithGroupData from 'test_fixtures/graphql/runner/get_runner.query.graphql.with_group.json'; import runnerProjectsData from 'test_fixtures/graphql/runner/get_runner_projects.query.graphql.json'; +import runnerJobsData from 'test_fixtures/graphql/runner/get_runner_jobs.query.graphql.json'; // Group queries import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json'; @@ -20,6 +21,7 @@ export { runnerData, runnerWithGroupData, runnerProjectsData, + runnerJobsData, groupRunnersData, groupRunnersCountData, groupRunnersDataPaginated, diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index 652fe0c4f94..747ff13c840 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -322,4 +322,102 @@ RSpec.describe Gitlab::Ci::Lint do end end end + + context 'pipeline logger' do + let(:counters) do + { + 'count' => a_kind_of(Numeric), + 'avg' => a_kind_of(Numeric), + 'max' => a_kind_of(Numeric), + 'min' => a_kind_of(Numeric) + } + end + + let(:loggable_data) do + { + 'class' => 'Gitlab::Ci::Pipeline::Logger', + 'config_build_context_duration_s' => counters, + 'config_build_variables_duration_s' => counters, + 'config_compose_duration_s' => counters, + 'config_expand_duration_s' => counters, + 'config_external_process_duration_s' => counters, + 'config_stages_inject_duration_s' => counters, + 'config_tags_resolve_duration_s' => counters, + 'config_yaml_extend_duration_s' => counters, + 'config_yaml_load_duration_s' => counters, + 'pipeline_creation_caller' => 'Gitlab::Ci::Lint', + 'pipeline_creation_service_duration_s' => a_kind_of(Numeric), + 'pipeline_persisted' => false, + 'pipeline_source' => 'unknown', + 'project_id' => project&.id, + 'yaml_process_duration_s' => counters + } + end + + let(:content) do + <<~YAML + build: + script: echo + YAML + end + + subject(:validate) { lint.validate(content, dry_run: false) } + + before do + project&.add_developer(user) + end + + context 'when the duration is under the threshold' do + it 'does not create a log entry' do + expect(Gitlab::AppJsonLogger).not_to receive(:info) + + validate + end + end + + context 'when the durations exceeds the threshold' do + let(:timer) do + proc do + @timer = @timer.to_i + 30 + end + end + + before do + allow(Gitlab::Ci::Pipeline::Logger) + .to receive(:current_monotonic_time) { timer.call } + end + + it 'creates a log entry' do + expect(Gitlab::AppJsonLogger).to receive(:info).with(loggable_data) + + validate + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(ci_pipeline_creation_logger: false) + end + + it 'does not create a log entry' do + expect(Gitlab::AppJsonLogger).not_to receive(:info) + + validate + end + end + + context 'when project is not provided' do + let(:project) { nil } + + let(:project_nil_loggable_data) do + loggable_data.except('project_id') + end + + it 'creates a log entry without project_id' do + expect(Gitlab::AppJsonLogger).to receive(:info).with(project_nil_loggable_data) + + validate + end + end + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/logger_spec.rb b/spec/lib/gitlab/ci/pipeline/logger_spec.rb index 1092824eddd..f31361431f2 100644 --- a/spec/lib/gitlab/ci/pipeline/logger_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/logger_spec.rb @@ -203,6 +203,35 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do expect(commit).to be_truthy end end + + context 'when project is not passed and pipeline is not persisted' do + let(:project) {} + let(:pipeline) { build(:ci_pipeline) } + + let(:loggable_data) do + { + 'class' => described_class.name.to_s, + 'pipeline_persisted' => false, + 'pipeline_creation_service_duration_s' => a_kind_of(Numeric), + 'pipeline_creation_caller' => 'source', + 'pipeline_save_duration_s' => { + 'avg' => 60, 'count' => 1, 'max' => 60, 'min' => 60 + }, + 'pipeline_creation_duration_s' => { + 'avg' => 20, 'count' => 2, 'max' => 30, 'min' => 10 + } + } + end + + it 'logs to application.json' do + expect(Gitlab::AppJsonLogger) + .to receive(:info) + .with(a_hash_including(loggable_data)) + .and_call_original + + expect(commit).to be_truthy + end + end end context 'when the feature flag is disabled' do diff --git a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb index 297bcc27bcd..ed11699e494 100644 --- a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb +++ b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb @@ -18,6 +18,15 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do )) end + context 'ensure keys are sorted' do + it 'does not have any keys that are out of order' do + parsed = YAML.parse_file(described_class.loose_foreign_keys_yaml_path) + mapping = parsed.children.first + table_names = mapping.children.select(&:scalar?).map(&:value) + expect(table_names).to eq(table_names.sort), "expected sorted table names in the YAML file" + end + end + context 'ensure no duplicates are found' do it 'does not have duplicate tables defined' do # since we use hash to detect duplicate hash keys we need to parse YAML document diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 5ec7c338a2a..b3b7c81e9e7 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -104,6 +104,34 @@ RSpec.describe Gitlab::Database do end end + describe '.check_for_non_superuser' do + subject { described_class.check_for_non_superuser } + + let(:non_superuser) { Gitlab::Database::PgUser.new(usename: 'foo', usesuper: false ) } + let(:superuser) { Gitlab::Database::PgUser.new(usename: 'bar', usesuper: true) } + + it 'prints user details if not superuser' do + allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(non_superuser) + + expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"foo\", UseSuper: (false)") + + subject + end + + it 'raises an exception if superuser' do + allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(superuser) + + expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"bar\", UseSuper: (true)") + expect { subject }.to raise_error('Error: detected superuser') + end + + it 'catches exception if find_by fails' do + allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_raise(ActiveRecord::StatementInvalid) + + expect { subject }.to raise_error('User CURRENT_USER not found') + end + end + describe '.check_postgres_version_and_print_warning' do let(:reflect) { instance_spy(Gitlab::Database::Reflection) } diff --git a/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb new file mode 100644 index 00000000000..1558facdf96 --- /dev/null +++ b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration!('fix_approval_rules_code_owners_rule_type_index') + +RSpec.describe FixApprovalRulesCodeOwnersRuleTypeIndex, :migration do + let(:table_name) { :approval_merge_request_rules } + let(:index_name) { 'index_approval_rules_code_owners_rule_type' } + + it 'correctly migrates up and down' do + reversible_migration do |migration| + migration.before -> { + expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy + } + + migration.after -> { + expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy + } + end + end + + context 'when the index already exists' do + before do + subject.add_concurrent_index table_name, :merge_request_id, where: 'rule_type = 2', name: index_name + end + + it 'keeps the index' do + migrate! + + expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy + end + end +end diff --git a/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb new file mode 100644 index 00000000000..b73aa7016a1 --- /dev/null +++ b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe UpdateDefaultScanMethodOfDastSiteProfile do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:dast_sites) { table(:dast_sites) } + let(:dast_site_profiles) { table(:dast_site_profiles) } + + before do + namespace = namespaces.create!(name: 'test', path: 'test') + project = projects.create!(id: 12, namespace_id: namespace.id, name: 'gitlab', path: 'gitlab') + dast_site = dast_sites.create!(id: 1, url: 'https://www.gitlab.com', project_id: project.id) + + dast_site_profiles.create!(id: 1, project_id: project.id, dast_site_id: dast_site.id, + name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 0", + scan_method: 0, target_type: 0) + + dast_site_profiles.create!(id: 2, project_id: project.id, dast_site_id: dast_site.id, + name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 1", + scan_method: 0, target_type: 1) + end + + it 'updates the scan_method to 1 for profiles with target_type 1' do + migrate! + + expect(dast_site_profiles.where(scan_method: 1).count).to eq 1 + expect(dast_site_profiles.where(scan_method: 0).count).to eq 1 + end +end diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb index 4e9aac01c08..c3fd8135ae0 100644 --- a/spec/tasks/gitlab/db_rake_spec.rb +++ b/spec/tasks/gitlab/db_rake_spec.rb @@ -446,6 +446,44 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do end end + describe 'gitlab:db:reset_as_non_superuser' do + let(:connection_pool) { instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool ) } + let(:connection) { instance_double(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) } + let(:configurations) { double(ActiveRecord::DatabaseConfigurations) } + let(:configuration) { instance_double(ActiveRecord::DatabaseConfigurations::HashConfig) } + let(:config_hash) { { username: 'foo' } } + + it 'migrate as nonsuperuser check with default username' do + allow(Rake::Task['db:drop']).to receive(:invoke) + allow(Rake::Task['db:create']).to receive(:invoke) + allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations) + allow(configurations).to receive(:configs_for).and_return([configuration]) + allow(configuration).to receive(:configuration_hash).and_return(config_hash) + allow(ActiveRecord::Base).to receive(:establish_connection).and_return(connection_pool) + + expect(config_hash).to receive(:merge).with({ username: 'gitlab' }) + expect(Gitlab::Database).to receive(:check_for_non_superuser) + expect(Rake::Task['db:migrate']).to receive(:invoke) + + run_rake_task('gitlab:db:reset_as_non_superuser') + end + + it 'migrate as nonsuperuser check with specified username' do + allow(Rake::Task['db:drop']).to receive(:invoke) + allow(Rake::Task['db:create']).to receive(:invoke) + allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations) + allow(configurations).to receive(:configs_for).and_return([configuration]) + allow(configuration).to receive(:configuration_hash).and_return(config_hash) + allow(ActiveRecord::Base).to receive(:establish_connection).and_return(connection_pool) + + expect(config_hash).to receive(:merge).with({ username: 'foo' }) + expect(Gitlab::Database).to receive(:check_for_non_superuser) + expect(Rake::Task['db:migrate']).to receive(:invoke) + + run_rake_task('gitlab:db:reset_as_non_superuser', '[foo]') + end + end + def run_rake_task(task_name, arguments = '') Rake::Task[task_name].reenable Rake.application.invoke_task("#{task_name}#{arguments}")