diff options
21 files changed, 567 insertions, 236 deletions
diff --git a/.gitlab/issue_templates/QA failure.md b/.gitlab/issue_templates/QA failure.md index e1b3eec5d29..2a8b1b2d2f9 100644 --- a/.gitlab/issue_templates/QA failure.md +++ b/.gitlab/issue_templates/QA failure.md @@ -31,6 +31,15 @@ Attach the screenshot and HTML snapshot of the page from the job's artifacts: 1. Open the `gitlab-qa-run-2020-*/gitlab-{ce,ee}-qa-*/{,ee}/{api,browser_ui}/<path to failed test>` folder. 1. Select the `.png` and `.html` files that appears in the job logs (look for `HTML screenshot: /path/to/html/page.html` / `Image screenshot: `/path/to/html/page.png`). 1. Drag and drop them here. + +Note: You don't need to include a screenshot if the information it contains can be included as text. Include the text instead. +E.g., error 500/404, "Retry later" errors, etc. + +If you include multiple screenshots it can be helpful to hide all but the first in a details/summary element, to avoid excessive scrolling: + +<details><summary>Expand for screenshot</summary> + drag and drop the screenshot here +</details> --> ### Possible fixes diff --git a/app/models/jira_import_state.rb b/app/models/jira_import_state.rb new file mode 100644 index 00000000000..0c138d674ea --- /dev/null +++ b/app/models/jira_import_state.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +class JiraImportState < ApplicationRecord + include AfterCommitQueue + + self.table_name = 'jira_imports' + + STATUSES = { initial: 0, scheduled: 1, started: 2, failed: 3, finished: 4 }.freeze + + belongs_to :project + belongs_to :user + belongs_to :label + + validates :project, presence: true + validates :jira_project_key, presence: true + validates :jira_project_name, presence: true + validates :jira_project_xid, presence: true + + validates :project, uniqueness: { + conditions: -> { where.not(status: STATUSES.values_at(:failed, :finished)) }, + message: _('Cannot have multiple Jira imports running at the same time') + } + + state_machine :status, initial: :initial do + event :schedule do + transition initial: :scheduled + end + + event :start do + transition scheduled: :started + end + + event :finish do + transition started: :finished + end + + event :do_fail do + transition [:initial, :scheduled, :started] => :failed + end + + after_transition initial: :scheduled do |state, _| + state.run_after_commit do + job_id = Gitlab::JiraImport::Stage::StartImportWorker.perform_async(project.id) + state.update(jid: job_id) if job_id + end + end + + after_transition any => :finished do |state, _| + if state.jid.present? + Gitlab::SidekiqStatus.unset(state.jid) + + state.update_column(:jid, nil) + end + end + + # Supress warning: + # both JiraImportState and its :status machine have defined a different default for "status". + # although both have same value but represented in 2 ways: integer(0) and symbol(:initial) + def owner_class_attribute_default + 'initial' + end + end + + enum status: STATUSES + + def in_progress? + scheduled? || started? + end + + def refresh_jid_expiration + return unless jid + + Gitlab::SidekiqStatus.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION) + end + + def self.jid_by(project_id:, status:) + select(:jid).with_status(status).find_by(project_id: project_id) + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 93ac9b64a98..cc7a732d94a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -314,6 +314,7 @@ class Project < ApplicationRecord has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project has_many :import_failures, inverse_of: :project + has_many :jira_imports, -> { order 'jira_imports.created_at' }, class_name: 'JiraImportState', inverse_of: :project has_many :daily_report_results, class_name: 'Ci::DailyReportResult' @@ -2424,6 +2425,10 @@ class Project < ApplicationRecord environments.where("name LIKE (#{::Gitlab::SQL::Glob.to_like(quoted_scope)})") # rubocop:disable GitlabSecurity/SqlInjection end + def latest_jira_import + jira_imports.last + end + private def find_service(services, name) diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md index b6f6faa6052..e2622cec6e2 100644 --- a/doc/development/testing_guide/end_to_end/index.md +++ b/doc/development/testing_guide/end_to_end/index.md @@ -30,7 +30,7 @@ You can find these nightly pipelines at `https://gitlab.com/gitlab-org/quality/s #### Using the `package-and-qa` job It is possible to run end-to-end tests for a merge request, eventually being run in -a pipeline in the [`gitlab-qa`](https://gitlab.com/gitlab-org/gitlab-qa/) project, +a pipeline in the [`gitlab-qa-mirror`](https://gitlab.com/gitlab-org/gitlab-qa-mirror/) project, by triggering the `package-and-qa` manual action in the `test` stage (not available for forks). @@ -49,8 +49,8 @@ pipelines. ```mermaid graph LR - A1 -.->|1. Triggers an omnibus-gitlab pipeline and wait for it to be done| A2 - B2[`Trigger-qa` stage<br>`Trigger:qa-test` job] -.->|2. Triggers a gitlab-qa pipeline and wait for it to be done| A3 + A1 -.->|1. Triggers an omnibus-gitlab-mirror pipeline and wait for it to be done| A2 + B2[`Trigger-qa` stage<br>`Trigger:qa-test` job] -.->|2. Triggers a gitlab-qa-mirror pipeline and wait for it to be done| A3 subgraph "gitlab-foss/gitlab pipeline" A1[`test` stage<br>`package-and-qa` job] @@ -60,7 +60,7 @@ subgraph "omnibus-gitlab pipeline" A2[`Trigger-docker` stage<br>`Trigger:gitlab-docker` job] -->|once done| B2 end -subgraph "gitlab-qa pipeline" +subgraph "gitlab-qa-mirror pipeline" A3>QA jobs run] -.->|3. Reports back the pipeline result to the `package-and-qa` job<br>and post the result on the original commit tested| A1 end ``` @@ -68,7 +68,7 @@ subgraph "gitlab-qa pipeline" 1. Developer triggers a manual action, that can be found in CE / EE merge requests. This starts a chain of pipelines in multiple projects. -1. The script being executed triggers a pipeline in [Omnibus GitLab][omnibus-gitlab] +1. The script being executed triggers a pipeline in [Omnibus GitLab Mirror][omnibus-gitlab-mirror] and waits for the resulting status. We call this a _status attribution_. 1. GitLab packages are being built in the [Omnibus GitLab][omnibus-gitlab] @@ -76,7 +76,7 @@ subgraph "gitlab-qa pipeline" 1. When packages are ready, and available in the registry, a final step in the [Omnibus GitLab][omnibus-gitlab] pipeline, triggers a new - GitLab QA pipeline (those with access can view them at `https://gitlab.com/gitlab-org/gitlab-qa/pipelines`). It also waits for a resulting status. + GitLab QA pipeline (those with access can view them at `https://gitlab.com/gitlab-org/gitlab-qa-mirror/pipelines`). It also waits for a resulting status. 1. GitLab QA pulls images from the registry, spins-up containers and runs tests against a test environment that has been just orchestrated by the `gitlab-qa` @@ -86,12 +86,12 @@ subgraph "gitlab-qa pipeline" propagated upstream, through Omnibus, back to the CE / EE merge request. Please note, we plan to [add more specific information](https://gitlab.com/gitlab-org/quality/team-tasks/issues/156) -about the tests included in each job/scenario that runs in `gitlab-qa`. +about the tests included in each job/scenario that runs in `gitlab-qa-mirror`. #### With Pipeline for Merged Results In a Pipeline for Merged Results, the pipeline runs on a new ref that contains the merge result of the source and target branch. -However, this ref is not available to the `gitlab-qa` pipeline. +However, this ref is not available to the `gitlab-qa-mirror` pipeline. For this reason, the end-to-end tests on a Pipeline for Merged Results would use the head of the merge request source branch. @@ -112,7 +112,7 @@ C --> D["Pipeline for merged results"] ##### Running custom tests The [existing scenarios](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md) -that run in the downstream `gitlab-qa` pipeline include many tests, but there are times when you might want to run a +that run in the downstream `gitlab-qa-mirror` pipeline include many tests, but there are times when you might want to run a test or a group of tests that are different than the groups in any of the existing scenarios. For example, when we [dequarantine](https://about.gitlab.com/handbook/engineering/quality/guidelines/debugging-qa-test-failures/#dequarantining-tests) @@ -197,6 +197,7 @@ you can find an issue you would like to work on in [the `gitlab-qa` issue tracker][gitlab-qa-issues]. [omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab +[omnibus-gitlab-mirror]: https://gitlab.com/gitlab-org/omnibus-gitlab-mirror [gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa [gitlab-qa-readme]: https://gitlab.com/gitlab-org/gitlab-qa/tree/master/README.md [review-apps]: ../review_apps.md diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md index 9cf58353991..961ac3d24c6 100644 --- a/doc/subscriptions/index.md +++ b/doc/subscriptions/index.md @@ -64,7 +64,7 @@ source projects, GitLab grants access to **Gold** features for all GitLab.com #### Self-managed -A self-managed subscription uses a hybrid model. You pay for a subscription according to the maximum number of users enabled during the subscription period. For instances that aren't air-gapped or on a closed network, the maximum number of simultaneous users in the self-managed installation is checked each quarter, using [Seat Link](#seat-link). +A self-managed subscription uses a hybrid model. You pay for a subscription according to the maximum number of users enabled during the subscription period. For instances that aren't offline or on a closed network, the maximum number of simultaneous users in the self-managed installation is checked each quarter, using [Seat Link](#seat-link). Every occupied seat, whether by person, job, or bot is counted in the subscription, with the following exceptions: @@ -255,7 +255,7 @@ Seat Link provides **only** the following information to GitLab: - Historical maximum user count - Active users count -For air-gapped or closed network customers, the existing [true-up model](#users-over-license) will be used. Prorated charges are not possible without user count data. +For offline or closed network customers, the existing [true-up model](#users-over-license) will be used. Prorated charges are not possible without user count data. <details> <summary>Click here to view example content of a Seat Link POST request.</summary> diff --git a/doc/topics/airgap/index.md b/doc/topics/airgap/index.md index 77c01863d47..44589c7e5f8 100644 --- a/doc/topics/airgap/index.md +++ b/doc/topics/airgap/index.md @@ -1,7 +1,7 @@ -# Air-gapped GitLab +# Offline GitLab -Computers in an air-gapped network are isolated from the public internet as a security measure. -This page lists all the information available for running GitLab in an air-gapped environment. +Computers in an offline environment are isolated from the public internet as a security measure. This +page lists all the information available for running GitLab in an offline environment. ## Quick start @@ -14,7 +14,7 @@ Follow these best practices to use GitLab's features in an offline environment: - [Operating the GitLab Secure scanners in an offline environment](../../user/application_security/offline_deployments/index.md). -## Loading Docker images onto your air-gapped host +## Loading Docker images onto your offline host To use many GitLab features, including [security scans](../../user/application_security/index.md#working-in-an-offline-environment) @@ -22,13 +22,13 @@ and [Auto Devops](../autodevops/), the GitLab Runner must be able to fetch the relevant Docker images. The process for making these images available without direct access to the public internet -involves downloading the images then packaging and transferring them to the air-gapped host. -Here's an example of such a transfer: +involves downloading the images then packaging and transferring them to the offline host. Here's an +example of such a transfer: 1. Download Docker images from public internet. 1. Package Docker images as tar archives. -1. Transfer images to air-gapped environment. -1. Load transferred images into air-gapped Docker registry. +1. Transfer images to offline environment. +1. Load transferred images into offline Docker registry. ### Example image packager script @@ -51,7 +51,7 @@ done ### Example image loader script -This example loads the images from a bastion host to an air-gapped host. In certain configurations, +This example loads the images from a bastion host to an offline host. In certain configurations, physical media may be needed for such a transfer: ```sh diff --git a/doc/topics/airgap/quick_start_guide.md b/doc/topics/airgap/quick_start_guide.md index a9c41e2f2c8..8d0ff3558ce 100644 --- a/doc/topics/airgap/quick_start_guide.md +++ b/doc/topics/airgap/quick_start_guide.md @@ -1,4 +1,4 @@ -# Getting started with an air-gapped GitLab Installation +# Getting started with an offline GitLab Installation This is a step-by-step guide that helps you install, configure, and use a self-managed GitLab instance entirely offline. diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index 801cacac958..610e11b18a9 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -180,7 +180,7 @@ using environment variables. | `CLAIR_DB_CONNECTION_STRING` | This variable represents the [connection string](https://www.postgresql.org/docs/9.3/libpq-connect.html#AEN39692) to the [PostgreSQL server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db) database and **shouldn't be changed** unless you're running the image locally as described in the [Running the standalone Container Scanning Tool](#running-the-standalone-container-scanning-tool) section. The host value for the connection string must match the [alias](https://gitlab.com/gitlab-org/gitlab/-/blob/898c5da43504eba87b749625da50098d345b60d6/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml#L23) value of the `Container-Scanning.gitlab-ci.yml` template file, which defaults to `clair-vulnerabilities-db`. | `postgresql://postgres:password@clair-vulnerabilities-db:5432/postgres?sslmode=disable&statement_timeout=60000` | | `CI_APPLICATION_REPOSITORY` | Docker repository URL for the image to be scanned. | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` | | `CI_APPLICATION_TAG` | Docker respository tag for the image to be scanned. | `$CI_COMMIT_SHA` | -| `CLAIR_DB_IMAGE` | The Docker image name and tag for the [PostgreSQL server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes, or to refer to a locally hosted vulnerabilities database for an on-premise air-gapped installation. | `arminc/clair-db:latest` | +| `CLAIR_DB_IMAGE` | The Docker image name and tag for the [PostgreSQL server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes, or to refer to a locally hosted vulnerabilities database for an on-premise offline installation. | `arminc/clair-db:latest` | | `CLAIR_DB_IMAGE_TAG` | (**DEPRECATED - use `CLAIR_DB_IMAGE` instead**) The Docker image tag for the [PostgreSQL server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes. | `latest` | | `DOCKERFILE_PATH` | The path to the `Dockerfile` to be used for generating remediations. By default, the scanner will look for a file named `Dockerfile` in the root directory of the project, so this variable should only be configured if your `Dockerfile` is in a non-standard location, such as a subdirectory. See [Solutions for vulnerabilities](#solutions-for-vulnerabilities-auto-remediation) for more details. | `Dockerfile` | | `ADDITIONAL_CA_CERT_BUNDLE` | Bundle of CA certs that you want to trust. | "" | @@ -210,7 +210,7 @@ If you want to whitelist specific vulnerabilities, you'll need to: in the [whitelist example file](https://github.com/arminc/clair-scanner/blob/v12/example-whitelist.yaml). 1. Add the `clair-whitelist.yml` file to the Git repository of your project. -### Running Container Scanning in an offline environment deployment +### Running Container Scanning in an offline environment Container Scanning can be executed on an offline GitLab Ultimate installation by using the following process: diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index c595eee0f0a..e60cf095f3f 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -461,7 +461,7 @@ dast: The DAST job does not require the project's repository to be present when running, so by default [`GIT_STRATEGY`](../../../ci/yaml/README.md#git-strategy) is set to `none`. -## Running DAST in an offline environment deployment +## Running DAST in an offline environment DAST can be executed on an offline GitLab Ultimate installation by using the following process: diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 352fdb64d07..723f4b8acbc 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -167,7 +167,7 @@ The following variables are used for configuring specific analyzers (used for a | `DS_PIP_DEPENDENCY_PATH` | `gemnasium-python` | | Path to load Python pip dependencies from. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12412) in GitLab 12.2) | | `DS_PYTHON_VERSION` | `retire.js` | | Version of Python. If set to 2, dependencies are installed using Python 2.7 instead of Python 3.6. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12296) in GitLab 12.1)| | `MAVEN_CLI_OPTS` | `gemnasium-maven` | `"-DskipTests --batch-mode"` | List of command line arguments that will be passed to `maven` by the analyzer. See an example for [using private repos](#using-private-maven-repos). | -| `BUNDLER_AUDIT_UPDATE_DISABLED` | `bundler-audit` | `false` | Disable automatic updates for the `bundler-audit` analyzer. Useful if you're running Dependency Scanning in an offline, air-gapped environment.| +| `BUNDLER_AUDIT_UPDATE_DISABLED` | `bundler-audit` | `false` | Disable automatic updates for the `bundler-audit` analyzer. Useful if you're running Dependency Scanning in an offline environment. | | `BUNDLER_AUDIT_ADVISORY_DB_URL` | `bundler-audit` | `https://github.com/rubysec/ruby-advisory-db` | URL of the advisory database used by bundler-audit. | | `BUNDLER_AUDIT_ADVISORY_DB_REF_NAME` | `bundler-audit` | `master` | Git ref for the advisory database specified by `BUNDLER_AUDIT_ADVISORY_DB_URL`. | | `RETIREJS_JS_ADVISORY_DB` | `retire.js` | `https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/jsrepository.json` | Path or URL to Retire.js js vulnerability data file. | diff --git a/doc/user/application_security/offline_deployments/index.md b/doc/user/application_security/offline_deployments/index.md index e548d2128b4..4511b4e80d6 100644 --- a/doc/user/application_security/offline_deployments/index.md +++ b/doc/user/application_security/offline_deployments/index.md @@ -2,15 +2,15 @@ type: reference, howto --- -# Offline environment deployments +# Offline environments -It is possible to run most of the GitLab security scanners when not -connected to the internet. +It's possible to run most of the GitLab security scanners when not connected to the internet. -This document describes how to operate Secure Categories (that is, scanner types) in an offline environment. These instructions also apply to -self-managed installations that are secured, have security policies (for example, firewall policies), or are otherwise restricted from -accessing the full internet. GitLab refers to these deployments as _offline environment deployments_. -Other common names include: +This document describes how to operate Secure Categories (that is, scanner types) in an offline +environment. These instructions also apply to self-managed installations that are secured, have +security policies (for example, firewall policies), or are otherwise restricted from accessing the +full internet. GitLab refers to these environments as _offline environments_. Other common names +include: - Air-gapped environments - Limited connectivity environments @@ -21,13 +21,13 @@ These environments have physical barriers or security policies (for example, fir or limit internet access. These instructions are designed for physically disconnected networks, but can also be followed in these other use cases. -## Offline environments +## Defining offline environments -In this situation, the GitLab instance can be one or more servers and services that can communicate -on a local network, but with no or very restricted access to the internet. Assume anything within -the GitLab instance and supporting infrastructure (for example, a private Maven repository) can be -accessed through a local network connection. Assume any files from the internet must come in through -physical media (USB drive, hard drive, writeable DVD, etc.). +In an offline environment, the GitLab instance can be one or more servers and services that can +communicate on a local network, but with no or very restricted access to the internet. Assume +anything within the GitLab instance and supporting infrastructure (for example, a private Maven +repository) can be accessed through a local network connection. Assume any files from the internet +must come in through physical media (USB drive, hard drive, writeable DVD, etc.). ## Overview @@ -43,7 +43,7 @@ an internet-connected GitLab installation, GitLab checks the GitLab.com-hosted container registry to check that you have the latest versions of these Docker images and possibly connect to package repositories to install necessary dependencies. -In an air-gapped environment, these checks must be disabled so that GitLab.com is not +In an offline environment, these checks must be disabled so that GitLab.com isn't queried. Because the GitLab.com registry and repositories are not available, you must update each of the scanners to either reference a different, internally-hosted registry or provide access to the individual scanner images. @@ -55,9 +55,11 @@ mirroring the packages inside your own offline network. ### Interacting with the vulnerabilities -Once a vulnerability is found, you can interact with it. Read more on how to [interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities). +Once a vulnerability is found, you can interact with it. Read more on how to +[interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities). -Please note that in some cases the reported vulnerabilities provide metadata that can contain external links exposed in the UI. These links might not be accessible within an air-gapped (or offline) environment. +Please note that in some cases the reported vulnerabilities provide metadata that can contain +external links exposed in the UI. These links might not be accessible within an offline environment. ### Scanner signature and rule updates @@ -73,6 +75,6 @@ hosted within your network. Each individual scanner may be slightly different than the steps described above. You can find more info at each of the pages below: -- [Container scanning offline directions](../container_scanning/index.md#running-container-scanning-in-an-offline-environment-deployment) -- [SAST offline directions](../sast/index.md#gitlab-sast-in-an-offline-environment-deployment) -- [DAST offline directions](../dast/index.md#running-dast-in-an-offline-environment-deployment) +- [Container scanning offline directions](../container_scanning/index.md#running-container-scanning-in-an-offline-environment) +- [SAST offline directions](../sast/index.md#gitlab-sast-in-an-offline-environment) +- [DAST offline directions](../dast/index.md#running-dast-in-an-offline-environment) diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index 7d9717b049d..64a8b1b40dd 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -491,10 +491,10 @@ Once a vulnerability is found, you can interact with it. Read more on how to For more information about the vulnerabilities database update, check the [maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database). -## GitLab SAST in an offline environment deployment +## GitLab SAST in an offline environment For self-managed GitLab instances in an environment with limited, restricted, or intermittent access -to external resources via the internet, some adjustments are required for the SAST job to +to external resources through the internet, some adjustments are required for the SAST job to successfully run. ### Requirements for offline SAST diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 92466dab033..ef49359b15b 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -93,7 +93,7 @@ A single `git push` is limited to 5GB. LFS is not affected by this limit. ## IP range GitLab.com is using the IP range `34.74.90.64/28` for traffic from its Web/API -fleet. You can expect connections from webhooks or repository mirroring to come +fleet. This whole range is solely allocated to GitLab. You can expect connections from webhooks or repository mirroring to come from those IPs and whitelist them. GitLab.com is fronted by Cloudflare. For incoming connections to GitLab.com you might need to whitelist CIDR blocks of Cloudflare ([IPv4](https://www.cloudflare.com/ips-v4) and [IPv6](https://www.cloudflare.com/ips-v6)) diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb index 4339d2ef490..f19dbe563ac 100644 --- a/lib/api/internal/pages.rb +++ b/lib/api/internal/pages.rb @@ -5,7 +5,6 @@ module API module Internal class Pages < Grape::API before do - not_found! unless Feature.enabled?(:pages_internal_api) authenticate_gitlab_pages_request! end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 983a142590f..8c6a1f54b9c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3353,6 +3353,9 @@ msgstr "" msgid "Cannot create the abuse report. This user has been blocked." msgstr "" +msgid "Cannot have multiple Jira imports running at the same time" +msgstr "" + msgid "Cannot make epic confidential if it contains not-confidential issues" msgstr "" diff --git a/spec/factories/jira_import_states.rb b/spec/factories/jira_import_states.rb new file mode 100644 index 00000000000..0fd6a58bbf1 --- /dev/null +++ b/spec/factories/jira_import_states.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :jira_import_state do + project + user { project&.creator } + label + jira_project_name { generate(:name) } + jira_project_key { generate(:name) } + jira_project_xid { 1234 } + end + + trait :scheduled do + status { :scheduled } + end + + trait :started do + status { :started } + end + + trait :failed do + status { :failed } + end + + trait :finished do + status { :finished } + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 1579a6ce2df..aaaf6458ee7 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -476,6 +476,7 @@ project: - requirements - export_jobs - daily_report_results +- jira_imports award_emoji: - awardable - user diff --git a/spec/models/jira_import_state_spec.rb b/spec/models/jira_import_state_spec.rb new file mode 100644 index 00000000000..f75a17f71b2 --- /dev/null +++ b/spec/models/jira_import_state_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe JiraImportState do + describe "associations" do + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:label) } + end + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(AfterCommitQueue) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:jira_project_key) } + it { is_expected.to validate_presence_of(:jira_project_name) } + it { is_expected.to validate_presence_of(:jira_project_xid) } + + context 'when trying to run multiple imports' do + let(:project) { create(:project) } + + context 'when project has an initial jira_import' do + let!(:jira_import) { create(:jira_import_state, project: project)} + + it_behaves_like 'multiple running imports not allowed' + end + + context 'when project has a scheduled jira_import' do + let!(:jira_import) { create(:jira_import_state, :scheduled, project: project)} + + it_behaves_like 'multiple running imports not allowed' + end + + context 'when project has a started jira_import' do + let!(:jira_import) { create(:jira_import_state, :started, project: project)} + + it_behaves_like 'multiple running imports not allowed' + end + + context 'when project has a failed jira_import' do + let!(:jira_import) { create(:jira_import_state, :failed, project: project)} + + it 'returns valid' do + new_import = build(:jira_import_state, project: project) + + expect(new_import).to be_valid + expect(new_import.errors[:project]).to be_empty + end + end + + context 'when project has a finished jira_import' do + let!(:jira_import) { create(:jira_import_state, :finished, project: project)} + + it 'returns valid' do + new_import = build(:jira_import_state, project: project) + + expect(new_import).to be_valid + expect(new_import.errors[:project]).to be_empty + end + end + end + end + + describe '#in_progress?' do + context 'statuses that return in progress' do + it_behaves_like 'in progress', :scheduled + it_behaves_like 'in progress', :started + end + + context 'statuses that return not in progress' do + it_behaves_like 'not in progress', :initial + it_behaves_like 'not in progress', :failed + it_behaves_like 'not in progress', :finished + end + end + + describe 'states transition flow' do + let(:project) { create(:project) } + + context 'when jira import is in initial state' do + let!(:jira_import) { build(:jira_import_state, project: project)} + + it_behaves_like 'can transition', [:schedule, :do_fail] + it_behaves_like 'cannot transition', [:start, :finish] + end + + context 'when jira import is in scheduled state' do + let!(:jira_import) { build(:jira_import_state, :scheduled, project: project)} + + it_behaves_like 'can transition', [:start, :do_fail] + it_behaves_like 'cannot transition', [:finish] + end + + context 'when jira import is in started state' do + let!(:jira_import) { build(:jira_import_state, :started, project: project)} + + it_behaves_like 'can transition', [:finish, :do_fail] + it_behaves_like 'cannot transition', [:schedule] + end + + context 'when jira import is in failed state' do + let!(:jira_import) { build(:jira_import_state, :failed, project: project)} + + it_behaves_like 'cannot transition', [:schedule, :finish, :do_fail] + end + + context 'when jira import is in finished state' do + let!(:jira_import) { build(:jira_import_state, :finished, project: project)} + + it_behaves_like 'cannot transition', [:schedule, :do_fail, :start] + end + + context 'after transition to scheduled' do + let!(:jira_import) { build(:jira_import_state, project: project)} + + it 'triggers the import job' do + expect(Gitlab::JiraImport::Stage::StartImportWorker).to receive(:perform_async).and_return('some-job-id') + + jira_import.schedule + + expect(jira_import.jid).to eq('some-job-id') + end + end + + context 'after transition to finished' do + let!(:jira_import) { build(:jira_import_state, :started, jid: 'some-other-jid', project: project)} + + it 'triggers the import job' do + jira_import.finish + + expect(jira_import.jid).to be_nil + end + + it 'triggers the import job' do + jira_import.update!(status: :scheduled) + + jira_import.finish + + expect(jira_import.status).to eq('scheduled') + expect(jira_import.jid).to eq('some-other-jid') + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1b12550ebac..0904ebca670 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -110,6 +110,7 @@ describe Project do it { is_expected.to have_many(:source_pipelines) } it { is_expected.to have_many(:prometheus_alert_events) } it { is_expected.to have_many(:self_managed_prometheus_alert_events) } + it { is_expected.to have_many(:jira_imports) } it_behaves_like 'model with repository' do let_it_be(:container) { create(:project, :repository, path: 'somewhere') } @@ -5987,6 +5988,34 @@ describe Project do end end + describe '#latest_jira_import' do + let_it_be(:project) { create(:project) } + context 'when no jira imports' do + it 'returns nil' do + expect(project.latest_jira_import).to be nil + end + end + + context 'when single jira import' do + let!(:jira_import1) { create(:jira_import_state, project: project) } + + it 'returns the jira import' do + expect(project.latest_jira_import).to eq(jira_import1) + end + end + + context 'when multiple jira imports' do + let!(:jira_import1) { create(:jira_import_state, :finished, created_at: 1.day.ago, project: project) } + let!(:jira_import2) { create(:jira_import_state, :failed, created_at: 2.days.ago, project: project) } + let!(:jira_import3) { create(:jira_import_state, :started, created_at: 3.days.ago, project: project) } + + it 'returns latest jira import by created_at' do + expect(project.jira_imports.pluck(:id)).to eq([jira_import3.id, jira_import2.id, jira_import1.id]) + expect(project.latest_jira_import).to eq(jira_import1) + end + end + end + def finish_job(export_job) export_job.start export_job.finish diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb index 44f7115f6a8..0c3c2fa22d6 100644 --- a/spec/requests/api/internal/pages_spec.rb +++ b/spec/requests/api/internal/pages_spec.rb @@ -14,241 +14,227 @@ describe API::Internal::Pages do get api("/internal/pages"), headers: headers, params: { host: host } end - context 'feature flag disabled' do - before do - stub_feature_flags(pages_internal_api: false) - end - - it 'responds with 404 Not Found' do + context 'not authenticated' do + it 'responds with 401 Unauthorized' do query_host('pages.gitlab.io') - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'feature flag enabled' do - context 'not authenticated' do - it 'responds with 401 Unauthorized' do - query_host('pages.gitlab.io') + context 'authenticated' do + def query_host(host) + jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256') + headers = { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token } - expect(response).to have_gitlab_http_status(:unauthorized) - end + super(host, headers) end - context 'authenticated' do - def query_host(host) - jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256') - headers = { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token } + def deploy_pages(project) + project.mark_pages_as_deployed + end - super(host, headers) - end + context 'domain does not exist' do + it 'responds with 204 no content' do + query_host('pages.gitlab.io') - def deploy_pages(project) - project.mark_pages_as_deployed + expect(response).to have_gitlab_http_status(:no_content) + expect(response.body).to be_empty end + end + + context 'serverless domain' do + let(:namespace) { create(:namespace, name: 'gitlab-org') } + let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') } + let(:environment) { create(:environment, project: project) } + let(:pages_domain) { create(:pages_domain, domain: 'serverless.gitlab.io') } + let(:knative_without_ingress) { create(:clusters_applications_knative) } + let(:knative_with_ingress) { create(:clusters_applications_knative, external_ip: '10.0.0.1') } + + context 'without a knative ingress gateway IP' do + let!(:serverless_domain_cluster) do + create( + :serverless_domain_cluster, + uuid: 'abcdef12345678', + pages_domain: pages_domain, + knative: knative_without_ingress + ) + end + + let(:serverless_domain) do + create( + :serverless_domain, + serverless_domain_cluster: serverless_domain_cluster, + environment: environment + ) + end - context 'domain does not exist' do it 'responds with 204 no content' do - query_host('pages.gitlab.io') + query_host(serverless_domain.uri.host) expect(response).to have_gitlab_http_status(:no_content) expect(response.body).to be_empty end end - context 'serverless domain' do - let(:namespace) { create(:namespace, name: 'gitlab-org') } - let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') } - let(:environment) { create(:environment, project: project) } - let(:pages_domain) { create(:pages_domain, domain: 'serverless.gitlab.io') } - let(:knative_without_ingress) { create(:clusters_applications_knative) } - let(:knative_with_ingress) { create(:clusters_applications_knative, external_ip: '10.0.0.1') } - - context 'without a knative ingress gateway IP' do - let!(:serverless_domain_cluster) do - create( - :serverless_domain_cluster, - uuid: 'abcdef12345678', - pages_domain: pages_domain, - knative: knative_without_ingress - ) - end - - let(:serverless_domain) do - create( - :serverless_domain, - serverless_domain_cluster: serverless_domain_cluster, - environment: environment - ) - end - - it 'responds with 204 no content' do - query_host(serverless_domain.uri.host) - - expect(response).to have_gitlab_http_status(:no_content) - expect(response.body).to be_empty - end + context 'with a knative ingress gateway IP' do + let!(:serverless_domain_cluster) do + create( + :serverless_domain_cluster, + uuid: 'abcdef12345678', + pages_domain: pages_domain, + knative: knative_with_ingress + ) end - context 'with a knative ingress gateway IP' do - let!(:serverless_domain_cluster) do - create( - :serverless_domain_cluster, - uuid: 'abcdef12345678', - pages_domain: pages_domain, - knative: knative_with_ingress - ) - end - - let(:serverless_domain) do - create( - :serverless_domain, - serverless_domain_cluster: serverless_domain_cluster, - environment: environment - ) - end - - it 'responds with proxy configuration' do - query_host(serverless_domain.uri.host) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('internal/serverless/virtual_domain') - - expect(json_response['certificate']).to eq(pages_domain.certificate) - expect(json_response['key']).to eq(pages_domain.key) - - expect(json_response['lookup_paths']).to eq( - [ - { - 'source' => { - 'type' => 'serverless', - 'service' => "test-function.#{project.name}-#{project.id}-#{environment.slug}.#{serverless_domain_cluster.knative.hostname}", - 'cluster' => { - 'hostname' => serverless_domain_cluster.knative.hostname, - 'address' => serverless_domain_cluster.knative.external_ip, - 'port' => 443, - 'cert' => serverless_domain_cluster.certificate, - 'key' => serverless_domain_cluster.key - } + let(:serverless_domain) do + create( + :serverless_domain, + serverless_domain_cluster: serverless_domain_cluster, + environment: environment + ) + end + + it 'responds with proxy configuration' do + query_host(serverless_domain.uri.host) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('internal/serverless/virtual_domain') + + expect(json_response['certificate']).to eq(pages_domain.certificate) + expect(json_response['key']).to eq(pages_domain.key) + + expect(json_response['lookup_paths']).to eq( + [ + { + 'source' => { + 'type' => 'serverless', + 'service' => "test-function.#{project.name}-#{project.id}-#{environment.slug}.#{serverless_domain_cluster.knative.hostname}", + 'cluster' => { + 'hostname' => serverless_domain_cluster.knative.hostname, + 'address' => serverless_domain_cluster.knative.external_ip, + 'port' => 443, + 'cert' => serverless_domain_cluster.certificate, + 'key' => serverless_domain_cluster.key } } - ] - ) - end + } + ] + ) end end + end - context 'custom domain' do - let(:namespace) { create(:namespace, name: 'gitlab-org') } - let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') } - let!(:pages_domain) { create(:pages_domain, domain: 'pages.io', project: project) } + context 'custom domain' do + let(:namespace) { create(:namespace, name: 'gitlab-org') } + let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') } + let!(:pages_domain) { create(:pages_domain, domain: 'pages.io', project: project) } - context 'when there are no pages deployed for the related project' do - it 'responds with 204 No Content' do - query_host('pages.io') + context 'when there are no pages deployed for the related project' do + it 'responds with 204 No Content' do + query_host('pages.io') - expect(response).to have_gitlab_http_status(:no_content) - end + expect(response).to have_gitlab_http_status(:no_content) end + end - context 'when there are pages deployed for the related project' do - it 'domain lookup is case insensitive' do - deploy_pages(project) + context 'when there are pages deployed for the related project' do + it 'domain lookup is case insensitive' do + deploy_pages(project) - query_host('Pages.IO') + query_host('Pages.IO') - expect(response).to have_gitlab_http_status(:ok) - end + expect(response).to have_gitlab_http_status(:ok) + end - it 'responds with the correct domain configuration' do - deploy_pages(project) + it 'responds with the correct domain configuration' do + deploy_pages(project) - query_host('pages.io') + query_host('pages.io') - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('internal/pages/virtual_domain') + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('internal/pages/virtual_domain') - expect(json_response['certificate']).to eq(pages_domain.certificate) - expect(json_response['key']).to eq(pages_domain.key) + expect(json_response['certificate']).to eq(pages_domain.certificate) + expect(json_response['key']).to eq(pages_domain.key) - expect(json_response['lookup_paths']).to eq( - [ - { - 'project_id' => project.id, - 'access_control' => false, - 'https_only' => false, - 'prefix' => '/', - 'source' => { - 'type' => 'file', - 'path' => 'gitlab-org/gitlab-ce/public/' - } + expect(json_response['lookup_paths']).to eq( + [ + { + 'project_id' => project.id, + 'access_control' => false, + 'https_only' => false, + 'prefix' => '/', + 'source' => { + 'type' => 'file', + 'path' => 'gitlab-org/gitlab-ce/public/' } - ] - ) - end + } + ] + ) end end + end - context 'namespaced domain' do - let(:group) { create(:group, name: 'mygroup') } + context 'namespaced domain' do + let(:group) { create(:group, name: 'mygroup') } - before do - allow(Settings.pages).to receive(:host).and_return('gitlab-pages.io') - allow(Gitlab.config.pages).to receive(:url).and_return("http://gitlab-pages.io") - end + before do + allow(Settings.pages).to receive(:host).and_return('gitlab-pages.io') + allow(Gitlab.config.pages).to receive(:url).and_return("http://gitlab-pages.io") + end - context 'regular project' do - it 'responds with the correct domain configuration' do - project = create(:project, group: group, name: 'myproject') - deploy_pages(project) - - query_host('mygroup.gitlab-pages.io') - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('internal/pages/virtual_domain') - - expect(json_response['lookup_paths']).to eq( - [ - { - 'project_id' => project.id, - 'access_control' => false, - 'https_only' => false, - 'prefix' => '/myproject/', - 'source' => { - 'type' => 'file', - 'path' => 'mygroup/myproject/public/' - } + context 'regular project' do + it 'responds with the correct domain configuration' do + project = create(:project, group: group, name: 'myproject') + deploy_pages(project) + + query_host('mygroup.gitlab-pages.io') + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('internal/pages/virtual_domain') + + expect(json_response['lookup_paths']).to eq( + [ + { + 'project_id' => project.id, + 'access_control' => false, + 'https_only' => false, + 'prefix' => '/myproject/', + 'source' => { + 'type' => 'file', + 'path' => 'mygroup/myproject/public/' } - ] - ) - end + } + ] + ) end + end - context 'group root project' do - it 'responds with the correct domain configuration' do - project = create(:project, group: group, name: 'mygroup.gitlab-pages.io') - deploy_pages(project) - - query_host('mygroup.gitlab-pages.io') - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('internal/pages/virtual_domain') - - expect(json_response['lookup_paths']).to eq( - [ - { - 'project_id' => project.id, - 'access_control' => false, - 'https_only' => false, - 'prefix' => '/', - 'source' => { - 'type' => 'file', - 'path' => 'mygroup/mygroup.gitlab-pages.io/public/' - } + context 'group root project' do + it 'responds with the correct domain configuration' do + project = create(:project, group: group, name: 'mygroup.gitlab-pages.io') + deploy_pages(project) + + query_host('mygroup.gitlab-pages.io') + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('internal/pages/virtual_domain') + + expect(json_response['lookup_paths']).to eq( + [ + { + 'project_id' => project.id, + 'access_control' => false, + 'https_only' => false, + 'prefix' => '/', + 'source' => { + 'type' => 'file', + 'path' => 'mygroup/mygroup.gitlab-pages.io/public/' } - ] - ) - end + } + ] + ) end end end diff --git a/spec/support/shared_examples/models/jira_import_state_shared_examples.rb b/spec/support/shared_examples/models/jira_import_state_shared_examples.rb new file mode 100644 index 00000000000..f4643375c8e --- /dev/null +++ b/spec/support/shared_examples/models/jira_import_state_shared_examples.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +shared_examples 'multiple running imports not allowed' do + it 'returns not valid' do + new_import = build(:jira_import_state, project: project) + + expect(new_import).not_to be_valid + expect(new_import.errors[:project]).not_to be_nil + end +end + +shared_examples 'in progress' do |status| + it 'returns true' do + jira_import_state = build(:jira_import_state, status: status) + expect(jira_import_state).to be_in_progress + end +end + +shared_examples 'not in progress' do |status| + it 'returns false' do + jira_import_state = build(:jira_import_state, status: status) + expect(jira_import_state).not_to be_in_progress + end +end + +shared_examples 'can transition' do |states| + states.each do |state| + it 'returns true' do + expect(jira_import.send(state)).to be true + end + end +end + +shared_examples 'cannot transition' do |states| + states.each do |state| + it 'returns false' do + expect(jira_import.send(state)).to be false + end + end +end |