From 2f147005c51bb8f59827ae9cf7484b8d64e7b549 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Sat, 11 Jul 2020 00:09:17 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo.yml | 6 - app/controllers/application_controller.rb | 4 +- app/controllers/chaos_controller.rb | 1 - app/controllers/concerns/issuable_actions.rb | 4 +- app/controllers/groups/application_controller.rb | 8 +- app/controllers/omniauth_callbacks_controller.rb | 2 +- app/controllers/projects/application_controller.rb | 2 +- app/controllers/projects/blob_controller.rb | 2 +- app/controllers/projects/releases_controller.rb | 2 +- app/controllers/projects/tree_controller.rb | 4 +- .../resolvers/projects/jira_projects_resolver.rb | 2 +- app/services/access_token_validation_service.rb | 10 +- app/services/spam/spam_verdict_service.rb | 2 +- app/workers/delete_merged_branches_worker.rb | 1 - .../metrics/dashboards/templating_variables.md | 128 ++++ doc/operations/metrics/dashboards/variables.md | 59 ++ doc/operations/metrics/dashboards/yaml.md | 2 +- doc/user/admin_area/merge_requests_approvals.md | 8 +- doc/user/project/clusters/add_remove_clusters.md | 5 - doc/user/project/integrations/prometheus.md | 179 ----- lib/api/api.rb | 4 +- lib/api/ci/pipeline_schedules.rb | 217 ++++++ lib/api/ci/pipelines.rb | 189 +++++ lib/api/pipeline_schedules.rb | 215 ------ lib/api/pipelines.rb | 187 ----- lib/gitlab/ci/pipeline/chain/validate/abilities.rb | 2 +- .../ci/pipeline/chain/validate/repository.rb | 2 +- lib/gitlab/git_ref_validator.rb | 4 +- lib/gitlab/gitaly_client/repository_service.rb | 4 +- lib/gitlab/middleware/go.rb | 4 +- lib/gitlab/utils.rb | 2 +- lib/google_api/auth.rb | 2 +- spec/requests/api/ci/pipeline_schedules_spec.rb | 522 ++++++++++++++ spec/requests/api/ci/pipelines_spec.rb | 786 +++++++++++++++++++++ spec/requests/api/pipeline_schedules_spec.rb | 522 -------------- spec/requests/api/pipelines_spec.rb | 786 --------------------- 36 files changed, 1939 insertions(+), 1940 deletions(-) create mode 100644 doc/operations/metrics/dashboards/templating_variables.md create mode 100644 doc/operations/metrics/dashboards/variables.md create mode 100644 lib/api/ci/pipeline_schedules.rb create mode 100644 lib/api/ci/pipelines.rb delete mode 100644 lib/api/pipeline_schedules.rb delete mode 100644 lib/api/pipelines.rb create mode 100644 spec/requests/api/ci/pipeline_schedules_spec.rb create mode 100644 spec/requests/api/ci/pipelines_spec.rb delete mode 100644 spec/requests/api/pipeline_schedules_spec.rb delete mode 100644 spec/requests/api/pipelines_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index dc6249100bd..edb3921609d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -666,12 +666,6 @@ Style/RedundantFreeze: Style/RedundantInterpolation: Enabled: false -# Offense count: 33 -# Cop supports --auto-correct. -# Configuration parameters: AllowMultipleReturnValues. -Style/RedundantReturn: - Enabled: false - # Offense count: 801 # Cop supports --auto-correct. Style/RedundantSelf: diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 789213a5fc6..2595b646964 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -306,7 +306,7 @@ class ApplicationController < ActionController::Base return if session[:impersonator_id] || !current_user&.allow_password_authentication? if current_user&.password_expired? - return redirect_to new_profile_password_path + redirect_to new_profile_password_path end end @@ -364,7 +364,7 @@ class ApplicationController < ActionController::Base def require_email if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil? - return redirect_to profile_path, notice: _('Please complete your profile with email address') + redirect_to profile_path, notice: _('Please complete your profile with email address') end end diff --git a/app/controllers/chaos_controller.rb b/app/controllers/chaos_controller.rb index ac008165c16..e0d1f313fc7 100644 --- a/app/controllers/chaos_controller.rb +++ b/app/controllers/chaos_controller.rb @@ -45,7 +45,6 @@ class ChaosController < ActionController::Base unless Devise.secure_compare(chaos_secret_configured, chaos_secret_request) render plain: "To experience chaos, please set a valid `X-Chaos-Secret` header or `token` param", status: :unauthorized - return end end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 068b7bdae2a..c4dbce00593 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -197,13 +197,13 @@ module IssuableActions def authorize_destroy_issuable! unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable) - return access_denied! + access_denied! end end def authorize_admin_issuable! unless can?(current_user, :"admin_#{resource_name}", parent) - return access_denied! + access_denied! end end diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 84c8d7ada43..9c2e361e92f 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -30,25 +30,25 @@ class Groups::ApplicationController < ApplicationController def authorize_admin_group! unless can?(current_user, :admin_group, group) - return render_404 + render_404 end end def authorize_create_deploy_token! unless can?(current_user, :create_deploy_token, group) - return render_404 + render_404 end end def authorize_destroy_deploy_token! unless can?(current_user, :destroy_deploy_token, group) - return render_404 + render_404 end end def authorize_admin_group_member! unless can?(current_user, :admin_group_member, group) - return render_403 + render_403 end end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 4c595313cb6..706a4843117 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -200,7 +200,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def fail_login(user) error_message = user.errors.full_messages.to_sentence - return redirect_to omniauth_error_path(oauth['provider'], error: error_message) + redirect_to omniauth_error_path(oauth['provider'], error: error_message) end def fail_auth0_login diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 82f98a9e411..d78a9bf0666 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -42,7 +42,7 @@ class Projects::ApplicationController < ApplicationController def authorize_action!(action) unless can?(current_user, action, project) - return access_denied! + access_denied! end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 80c5fb470dd..49f658d9ffc 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -129,7 +129,7 @@ class Projects::BlobController < Projects::ApplicationController end end - return redirect_to_tree_root_for_missing_path(@project, @ref, @path) + redirect_to_tree_root_for_missing_path(@project, @ref, @path) end end diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 3d48fb9c803..d58755c2655 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -30,7 +30,7 @@ class Projects::ReleasesController < Projects::ApplicationController def new unless Feature.enabled?(:new_release_page, project) - return redirect_to(new_project_tag_path(@project)) + redirect_to(new_project_tag_path(@project)) end end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index aadc766313d..638e1a05c18 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -20,9 +20,9 @@ class Projects::TreeController < Projects::ApplicationController if tree.entries.empty? if @repository.blob_at(@commit.id, @path) - return redirect_to project_blob_path(@project, File.join(@ref, @path)) + redirect_to project_blob_path(@project, File.join(@ref, @path)) elsif @path.present? - return redirect_to_tree_root_for_missing_path(@project, @ref, @path) + redirect_to_tree_root_for_missing_path(@project, @ref, @path) end end end diff --git a/app/graphql/resolvers/projects/jira_projects_resolver.rb b/app/graphql/resolvers/projects/jira_projects_resolver.rb index 3b6e5c4fd42..2dc712128cc 100644 --- a/app/graphql/resolvers/projects/jira_projects_resolver.rb +++ b/app/graphql/resolvers/projects/jira_projects_resolver.rb @@ -37,7 +37,7 @@ module Resolvers def jira_projects(name:) args = { query: name }.compact - return Jira::Requests::Projects::ListService.new(project.jira_service, args).execute + Jira::Requests::Projects::ListService.new(project.jira_service, args).execute end end end diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index 851d862c0cf..eb2e66a9285 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -17,21 +17,21 @@ class AccessTokenValidationService def validate(scopes: []) if token.expired? - return EXPIRED + EXPIRED elsif token.revoked? - return REVOKED + REVOKED elsif !self.include_any_scope?(scopes) - return INSUFFICIENT_SCOPE + INSUFFICIENT_SCOPE elsif token.respond_to?(:impersonation) && token.impersonation && !Gitlab.config.gitlab.impersonation_enabled - return IMPERSONATION_DISABLED + IMPERSONATION_DISABLED else - return VALID + VALID end end diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb index be156a0ebeb..ba1c583c7af 100644 --- a/app/services/spam/spam_verdict_service.rb +++ b/app/services/spam/spam_verdict_service.rb @@ -57,7 +57,7 @@ module Spam rescue *Gitlab::HTTP::HTTP_ERRORS => e # @TODO: log error via try_post https://gitlab.com/gitlab-org/gitlab/-/issues/219223 Gitlab::ErrorTracking.log_exception(e) - return + nil rescue # @TODO log ALLOW diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb index ab3d42e5384..8d7026e2d1e 100644 --- a/app/workers/delete_merged_branches_worker.rb +++ b/app/workers/delete_merged_branches_worker.rb @@ -17,7 +17,6 @@ class DeleteMergedBranchesWorker # rubocop:disable Scalability/IdempotentWorker begin ::Branches::DeleteMergedService.new(project, user).execute rescue Gitlab::Access::AccessDeniedError - return end end end diff --git a/doc/operations/metrics/dashboards/templating_variables.md b/doc/operations/metrics/dashboards/templating_variables.md new file mode 100644 index 00000000000..a515742ea92 --- /dev/null +++ b/doc/operations/metrics/dashboards/templating_variables.md @@ -0,0 +1,128 @@ +--- +stage: Monitor +group: APM +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + +# Templating variables for metrics dashboards + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214539) in GitLab 13.0. + +Templating variables can be used to make your metrics dashboard more versatile. + +`templating` is a top-level key in the +[dashboard YAML](yaml.md#dashboard-top-level-properties). +Define your variables in the `variables` key, under `templating`. The value of +the `variables` key should be a hash, and each key under `variables` +defines a templating variable on the dashboard, and may contain alphanumeric and underscore characters. + +A variable can be used in a Prometheus query in the same dashboard using the syntax +described [in Using Variables](variables.md). + +## `text` variable type + +CAUTION: **Warning:** +This variable type is an _alpha_ feature, and is subject to change at any time +without prior notice! + +For each `text` variable defined in the dashboard YAML, there will be a free text +box on the dashboard UI, allowing you to enter a value for each variable. + +The `text` variable type supports a simple and a full syntax. + +### Simple syntax + +This example creates a variable called `variable1`, with a default value +of `default value`: + +```yaml +templating: + variables: + variable1: 'default value' # `text` type variable with `default value` as its default. +``` + +### Full syntax + +This example creates a variable called `variable1`, with a default value of `default`. +The label for the text box on the UI will be the value of the `label` key: + +```yaml +templating: + variables: + variable1: # The variable name that can be used in queries. + label: 'Variable 1' # (Optional) label that will appear in the UI for this text box. + type: text + options: + default_value: 'default' # (Optional) default value. +``` + +## `custom` variable type + +CAUTION: **Warning:** +This variable type is an _alpha_ feature, and is subject to change at any time +without prior notice! + +Each `custom` variable defined in the dashboard YAML creates a dropdown +selector on the dashboard UI, allowing you to select a value for each variable. + +The `custom` variable type supports a simple and a full syntax. + +### Simple syntax + +This example creates a variable called `variable1`, with a default value of `value1`. +The dashboard UI will display a dropdown with `value1`, `value2` and `value3` +as the choices. + +```yaml +templating: + variables: + variable1: ['value1', 'value2', 'value3'] +``` + +### Full syntax + +This example creates a variable called `variable1`, with a default value of `value_option_2`. +The label for the text box on the UI will be the value of the `label` key. +The dashboard UI will display a dropdown with `Option 1` and `Option 2` +as the choices. + +If you select `Option 1` from the dropdown, the variable will be replaced with `value option 1`. +Similarly, if you select `Option 2`, the variable will be replaced with `value_option_2`: + +```yaml +templating: + variables: + variable1: # The variable name that can be used in queries. + label: 'Variable 1' # (Optional) label that will appear in the UI for this dropdown. + type: custom + options: + values: + - value: 'value option 1' # The value that will replace the variable in queries. + text: 'Option 1' # (Optional) Text that will appear in the UI dropdown. + - value: 'value_option_2' + text: 'Option 2' + default: true # (Optional) This option should be the default value of this variable. +``` + +## `metric_label_values` variable type + +CAUTION: **Warning:** +This variable type is an _alpha_ feature, and is subject to change at any time +without prior notice! + +### Full syntax + +This example creates a variable called `variable2`. The values of the dropdown will +be all the different values of the `backend` label in the Prometheus series described by +`up{env="production"}`. + +```yaml +templating: + variables: + variable2: # The variable name that can be interpolated in queries. + label: 'Variable 2' # (Optional) label that will appear in the UI for this dropdown. + type: metric_label_values + options: + series_selector: 'up{env="production"}' + label: 'backend' +``` diff --git a/doc/operations/metrics/dashboards/variables.md b/doc/operations/metrics/dashboards/variables.md new file mode 100644 index 00000000000..19b77a1ed87 --- /dev/null +++ b/doc/operations/metrics/dashboards/variables.md @@ -0,0 +1,59 @@ +--- +stage: Monitor +group: APM +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + +# Using Variables + +## Query Variables + +Variables can be specified using double curly braces, such as `"{{ci_environment_slug}}"` ([added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.7). + +Support for the `"%{ci_environment_slug}"` format was +[removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31581) in GitLab 13.0. +Queries that continue to use the old format will show no data. + +## Predefined variables + +GitLab supports a limited set of [CI variables](../../../ci/variables/README.md) in the Prometheus query. This is particularly useful for identifying a specific environment, for example with `ci_environment_slug`. The supported variables are: + +- `ci_environment_slug` +- `kube_namespace` +- `ci_project_name` +- `ci_project_namespace` +- `ci_project_path` +- `ci_environment_name` +- `__range` + +NOTE: **Note:** +Variables for Prometheus queries must be lowercase. + +### __range + +The `__range` variable is useful in Prometheus +[range vector selectors](https://prometheus.io/docs/prometheus/latest/querying/basics/#range-vector-selectors). +Its value is the total number of seconds in the dashboard's time range. +For example, if the dashboard time range is set to 8 hours, the value of +`__range` is `28800s`. + +## User-defined variables + +[Variables can be defined](../../../operations/metrics/dashboards/yaml.md#templating-templating-properties) in a custom dashboard YAML file. + +## Query Variables from URL + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214500) in GitLab 13.0. + +GitLab supports setting custom variables through URL parameters. Surround the variable +name with double curly braces (`{{example}}`) to interpolate the variable in a query: + +```plaintext +avg(sum(container_memory_usage_bytes{container_name!="{{pod}}"}) by (job)) without (job) /1024/1024/1024' +``` + +The URL for this query would be: + +```plaintext +http://gitlab.com///-/environments//metrics?dashboard=.gitlab%2Fdashboards%2Fcustom.yml&pod=POD +``` diff --git a/doc/operations/metrics/dashboards/yaml.md b/doc/operations/metrics/dashboards/yaml.md index 501bfd29eb5..78d05b79492 100644 --- a/doc/operations/metrics/dashboards/yaml.md +++ b/doc/operations/metrics/dashboards/yaml.md @@ -29,7 +29,7 @@ The following tables outline the details of expected properties. | -------- | ---- | -------- | ----------- | | `variables` | hash | yes | Variables can be defined here. | -Read the documentation on [templating](../../../user/project/integrations/prometheus.md#templating-variables-for-metrics-dashboards). +Read the documentation on [templating](templating_variables.md). ## **Links (`links`) properties** diff --git a/doc/user/admin_area/merge_requests_approvals.md b/doc/user/admin_area/merge_requests_approvals.md index f380ff7b211..ac5ac98c54d 100644 --- a/doc/user/admin_area/merge_requests_approvals.md +++ b/doc/user/admin_area/merge_requests_approvals.md @@ -58,13 +58,13 @@ Maintainer role and above can modify these. This feature comes with two feature flags which are disabled by default. -- The configuration in Admin area is controlled via `admin_merge_request_approval_settings` -- The application of these rules is controlled via `project_merge_request_approval_settings` +- The configuration in Admin area is controlled via `admin_compliance_merge_request_approval_settings`. +- The application of these rules is controlled via `project_compliance_merge_request_approval_settings`. These feature flags can be managed by feature flag [API endpoint](../../api/features.md#set-or-create-a-feature) or by [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) with the following commands: ```ruby -Feature.enable(:admin_merge_request_approval_settings) -Feature.enable(:project_merge_request_approval_settings) +Feature.enable(:admin_compliance_merge_request_approval_settings) +Feature.enable(:project_compliance_merge_request_approval_settings) ``` diff --git a/doc/user/project/clusters/add_remove_clusters.md b/doc/user/project/clusters/add_remove_clusters.md index 7deab9aae85..f581383022e 100644 --- a/doc/user/project/clusters/add_remove_clusters.md +++ b/doc/user/project/clusters/add_remove_clusters.md @@ -153,11 +153,6 @@ Amazon Elastic Kubernetes Service (EKS) at the project, group, or instance level If you have an existing Kubernetes cluster, you can add it to a project, group, or instance. -For more information, see information for adding an: - -- [Existing Kubernetes cluster](#existing-kubernetes-cluster), including GKE clusters. -- [Existing EKS cluster](add_eks_clusters.md#existing-eks-cluster). - NOTE: **Note:** Kubernetes integration is not supported for arm64 clusters. See the issue [Helm Tiller fails to install on arm64 cluster](https://gitlab.com/gitlab-org/gitlab/-/issues/29838) for details. diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 445516b29cc..f038aa4a7de 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -168,60 +168,6 @@ A few fields are required: Multiple metrics can be displayed on the same chart if the fields **Name**, **Type**, and **Y-axis label** match between metrics. For example, a metric with **Name** `Requests Rate`, **Type** `Business`, and **Y-axis label** `rec / sec` would display on the same chart as a second metric with the same values. A **Legend label** is suggested if this feature is used. -#### Query Variables - -##### Predefined variables - -GitLab supports a limited set of [CI variables](../../../ci/variables/README.md) in the Prometheus query. This is particularly useful for identifying a specific environment, for example with `ci_environment_slug`. The supported variables are: - -- `ci_environment_slug` -- `kube_namespace` -- `ci_project_name` -- `ci_project_namespace` -- `ci_project_path` -- `ci_environment_name` -- `__range` - -NOTE: **Note:** -Variables for Prometheus queries must be lowercase. - -###### __range - -The `__range` variable is useful in Prometheus -[range vector selectors](https://prometheus.io/docs/prometheus/latest/querying/basics/#range-vector-selectors). -Its value is the total number of seconds in the dashboard's time range. -For example, if the dashboard time range is set to 8 hours, the value of -`__range` is `28800s`. - -##### User-defined variables - -[Variables can be defined](../../../operations/metrics/dashboards/yaml.md#templating-templating-properties) in a custom dashboard YAML file. - -##### Using variables - -Variables can be specified using double curly braces, such as `"{{ci_environment_slug}}"` ([added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.7). - -Support for the `"%{ci_environment_slug}"` format was -[removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31581) in GitLab 13.0. -Queries that continue to use the old format will show no data. - -#### Query Variables from URL - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214500) in GitLab 13.0. - -GitLab supports setting custom variables through URL parameters. Surround the variable -name with double curly braces (`{{example}}`) to interpolate the variable in a query: - -```plaintext -avg(sum(container_memory_usage_bytes{container_name!="{{pod}}"}) by (job)) without (job) /1024/1024/1024' -``` - -The URL for this query would be: - -```plaintext -http://gitlab.com///-/environments//metrics?dashboard=.gitlab%2Fdashboards%2Fcustom.yml&pod=POD -``` - #### Editing additional metrics from the dashboard > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208976) in GitLab 12.9. @@ -353,131 +299,6 @@ When **Metrics Dashboard YAML definition is invalid** at least one of the follow Metrics Dashboard YAML definition validation information is also available as a [GraphQL API field](../../../api/graphql/reference/index.md#metricsdashboard) -### Templating variables for metrics dashboards - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214539) in GitLab 13.0. - -Templating variables can be used to make your metrics dashboard more versatile. - -#### Templating variable types - -`templating` is a top-level key in the -[dashboard YAML](../../../operations/metrics/dashboards/yaml.md#dashboard-top-level-properties). -Define your variables in the `variables` key, under `templating`. The value of -the `variables` key should be a hash, and each key under `variables` -defines a templating variable on the dashboard, and may contain alphanumeric and underscore characters. - -A variable can be used in a Prometheus query in the same dashboard using the syntax -described [here](#using-variables). - -##### `text` variable type - -CAUTION: **Warning:** -This variable type is an _alpha_ feature, and is subject to change at any time -without prior notice! - -For each `text` variable defined in the dashboard YAML, there will be a free text -box on the dashboard UI, allowing you to enter a value for each variable. - -The `text` variable type supports a simple and a full syntax. - -###### Simple syntax - -This example creates a variable called `variable1`, with a default value -of `default value`: - -```yaml -templating: - variables: - variable1: 'default value' # `text` type variable with `default value` as its default. -``` - -###### Full syntax - -This example creates a variable called `variable1`, with a default value of `default`. -The label for the text box on the UI will be the value of the `label` key: - -```yaml -templating: - variables: - variable1: # The variable name that can be used in queries. - label: 'Variable 1' # (Optional) label that will appear in the UI for this text box. - type: text - options: - default_value: 'default' # (Optional) default value. -``` - -##### `custom` variable type - -CAUTION: **Warning:** -This variable type is an _alpha_ feature, and is subject to change at any time -without prior notice! - -Each `custom` variable defined in the dashboard YAML creates a dropdown -selector on the dashboard UI, allowing you to select a value for each variable. - -The `custom` variable type supports a simple and a full syntax. - -###### Simple syntax - -This example creates a variable called `variable1`, with a default value of `value1`. -The dashboard UI will display a dropdown with `value1`, `value2` and `value3` -as the choices. - -```yaml -templating: - variables: - variable1: ['value1', 'value2', 'value3'] -``` - -###### Full syntax - -This example creates a variable called `variable1`, with a default value of `value_option_2`. -The label for the text box on the UI will be the value of the `label` key. -The dashboard UI will display a dropdown with `Option 1` and `Option 2` -as the choices. - -If you select `Option 1` from the dropdown, the variable will be replaced with `value option 1`. -Similarly, if you select `Option 2`, the variable will be replaced with `value_option_2`: - -```yaml -templating: - variables: - variable1: # The variable name that can be used in queries. - label: 'Variable 1' # (Optional) label that will appear in the UI for this dropdown. - type: custom - options: - values: - - value: 'value option 1' # The value that will replace the variable in queries. - text: 'Option 1' # (Optional) Text that will appear in the UI dropdown. - - value: 'value_option_2' - text: 'Option 2' - default: true # (Optional) This option should be the default value of this variable. -``` - -##### `metric_label_values` variable type - -CAUTION: **Warning:** -This variable type is an _alpha_ feature, and is subject to change at any time -without prior notice! - -###### Full syntax - -This example creates a variable called `variable2`. The values of the dropdown will -be all the different values of the `backend` label in the Prometheus series described by -`up{env="production"}`. - -```yaml -templating: - variables: - variable2: # The variable name that can be interpolated in queries. - label: 'Variable 2' # (Optional) label that will appear in the UI for this dropdown. - type: metric_label_values - options: - series_selector: 'up{env="production"}' - label: 'backend' -``` - ### Add related links to custom dashboards > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216385) in GitLab 13.1. diff --git a/lib/api/api.rb b/lib/api/api.rb index 73e6bc4d767..bf685706bed 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -133,6 +133,8 @@ module API mount ::API::Boards mount ::API::Branches mount ::API::BroadcastMessages + mount ::API::Ci::Pipelines + mount ::API::Ci::PipelineSchedules mount ::API::Ci::Runner mount ::API::Ci::Runners mount ::API::Commits @@ -179,8 +181,6 @@ module API mount ::API::NotificationSettings mount ::API::Pages mount ::API::PagesDomains - mount ::API::Pipelines - mount ::API::PipelineSchedules mount ::API::ProjectClusters mount ::API::ProjectContainerRepositories mount ::API::ProjectEvents diff --git a/lib/api/ci/pipeline_schedules.rb b/lib/api/ci/pipeline_schedules.rb new file mode 100644 index 00000000000..80ad8aa04dd --- /dev/null +++ b/lib/api/ci/pipeline_schedules.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +module API + module Ci + class PipelineSchedules < Grape::API::Instance + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Get all pipeline schedules' do + success Entities::PipelineSchedule + end + params do + use :pagination + optional :scope, type: String, values: %w[active inactive], + desc: 'The scope of pipeline schedules' + end + # rubocop: disable CodeReuse/ActiveRecord + get ':id/pipeline_schedules' do + authorize! :read_pipeline_schedule, user_project + + schedules = ::Ci::PipelineSchedulesFinder.new(user_project).execute(scope: params[:scope]) + .preload([:owner, :last_pipeline]) + present paginate(schedules), with: Entities::PipelineSchedule + end + # rubocop: enable CodeReuse/ActiveRecord + + desc 'Get a single pipeline schedule' do + success Entities::PipelineScheduleDetails + end + params do + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + end + get ':id/pipeline_schedules/:pipeline_schedule_id' do + present pipeline_schedule, with: Entities::PipelineScheduleDetails + end + + desc 'Create a new pipeline schedule' do + success Entities::PipelineScheduleDetails + end + params do + requires :description, type: String, desc: 'The description of pipeline schedule' + requires :ref, type: String, desc: 'The branch/tag name will be triggered', allow_blank: false + requires :cron, type: String, desc: 'The cron' + optional :cron_timezone, type: String, default: 'UTC', desc: 'The timezone' + optional :active, type: Boolean, default: true, desc: 'The activation of pipeline schedule' + end + post ':id/pipeline_schedules' do + authorize! :create_pipeline_schedule, user_project + + pipeline_schedule = ::Ci::CreatePipelineScheduleService + .new(user_project, current_user, declared_params(include_missing: false)) + .execute + + if pipeline_schedule.persisted? + present pipeline_schedule, with: Entities::PipelineScheduleDetails + else + render_validation_error!(pipeline_schedule) + end + end + + desc 'Edit a pipeline schedule' do + success Entities::PipelineScheduleDetails + end + params do + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + optional :description, type: String, desc: 'The description of pipeline schedule' + optional :ref, type: String, desc: 'The branch/tag name will be triggered' + optional :cron, type: String, desc: 'The cron' + optional :cron_timezone, type: String, desc: 'The timezone' + optional :active, type: Boolean, desc: 'The activation of pipeline schedule' + end + put ':id/pipeline_schedules/:pipeline_schedule_id' do + authorize! :update_pipeline_schedule, pipeline_schedule + + if pipeline_schedule.update(declared_params(include_missing: false)) + present pipeline_schedule, with: Entities::PipelineScheduleDetails + else + render_validation_error!(pipeline_schedule) + end + end + + desc 'Take ownership of a pipeline schedule' do + success Entities::PipelineScheduleDetails + end + params do + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + end + post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do + authorize! :update_pipeline_schedule, pipeline_schedule + + if pipeline_schedule.own!(current_user) + present pipeline_schedule, with: Entities::PipelineScheduleDetails + else + render_validation_error!(pipeline_schedule) + end + end + + desc 'Delete a pipeline schedule' do + success Entities::PipelineScheduleDetails + end + params do + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + end + delete ':id/pipeline_schedules/:pipeline_schedule_id' do + authorize! :admin_pipeline_schedule, pipeline_schedule + + destroy_conditionally!(pipeline_schedule) + end + + desc 'Play a scheduled pipeline immediately' do + detail 'This feature was added in GitLab 12.8' + end + params do + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + end + post ':id/pipeline_schedules/:pipeline_schedule_id/play' do + authorize! :play_pipeline_schedule, pipeline_schedule + + job_id = RunPipelineScheduleWorker # rubocop:disable CodeReuse/Worker + .perform_async(pipeline_schedule.id, current_user.id) + + if job_id + created! + else + render_api_error!('Unable to schedule pipeline run immediately', 500) + end + end + + desc 'Create a new pipeline schedule variable' do + success Entities::Variable + end + params do + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + requires :key, type: String, desc: 'The key of the variable' + requires :value, type: String, desc: 'The value of the variable' + optional :variable_type, type: String, values: ::Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var' + end + post ':id/pipeline_schedules/:pipeline_schedule_id/variables' do + authorize! :update_pipeline_schedule, pipeline_schedule + + variable_params = declared_params(include_missing: false) + variable = pipeline_schedule.variables.create(variable_params) + if variable.persisted? + present variable, with: Entities::Variable + else + render_validation_error!(variable) + end + end + + desc 'Edit a pipeline schedule variable' do + success Entities::Variable + end + params do + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + requires :key, type: String, desc: 'The key of the variable' + optional :value, type: String, desc: 'The value of the variable' + optional :variable_type, type: String, values: ::Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file' + end + put ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do + authorize! :update_pipeline_schedule, pipeline_schedule + + if pipeline_schedule_variable.update(declared_params(include_missing: false)) + present pipeline_schedule_variable, with: Entities::Variable + else + render_validation_error!(pipeline_schedule_variable) + end + end + + desc 'Delete a pipeline schedule variable' do + success Entities::Variable + end + params do + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + requires :key, type: String, desc: 'The key of the variable' + end + delete ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do + authorize! :admin_pipeline_schedule, pipeline_schedule + + status :accepted + present pipeline_schedule_variable.destroy, with: Entities::Variable + end + end + + helpers do + # rubocop: disable CodeReuse/ActiveRecord + def pipeline_schedule + @pipeline_schedule ||= + user_project + .pipeline_schedules + .preload(:owner, :last_pipeline) + .find_by(id: params.delete(:pipeline_schedule_id)).tap do |pipeline_schedule| + unless can?(current_user, :read_pipeline_schedule, pipeline_schedule) + not_found!('Pipeline Schedule') + end + end + end + # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def pipeline_schedule_variable + @pipeline_schedule_variable ||= + pipeline_schedule.variables.find_by(key: params[:key]).tap do |pipeline_schedule_variable| + unless pipeline_schedule_variable + not_found!('Pipeline Schedule Variable') + end + end + end + # rubocop: enable CodeReuse/ActiveRecord + end + end + end +end diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb new file mode 100644 index 00000000000..33bb8b38d92 --- /dev/null +++ b/lib/api/ci/pipelines.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +module API + module Ci + class Pipelines < Grape::API::Instance + include PaginationParams + + before { authenticate_non_get! } + + params do + requires :id, type: String, desc: 'The project ID' + end + resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Get all Pipelines of the project' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::PipelineBasic + end + params do + use :pagination + optional :scope, type: String, values: %w[running pending finished branches tags], + desc: 'The scope of pipelines' + optional :status, type: String, values: ::Ci::HasStatus::AVAILABLE_STATUSES, + desc: 'The status of pipelines' + optional :ref, type: String, desc: 'The ref of pipelines' + optional :sha, type: String, desc: 'The sha of pipelines' + optional :yaml_errors, type: Boolean, desc: 'Returns pipelines with invalid configurations' + optional :name, type: String, desc: 'The name of the user who triggered pipelines' + optional :username, type: String, desc: 'The username of the user who triggered pipelines' + optional :updated_before, type: DateTime, desc: 'Return pipelines updated before the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ' + optional :updated_after, type: DateTime, desc: 'Return pipelines updated after the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ' + optional :order_by, type: String, values: ::Ci::PipelinesFinder::ALLOWED_INDEXED_COLUMNS, default: 'id', + desc: 'Order pipelines' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Sort pipelines' + end + get ':id/pipelines' do + authorize! :read_pipeline, user_project + authorize! :read_build, user_project + + pipelines = ::Ci::PipelinesFinder.new(user_project, current_user, params).execute + present paginate(pipelines), with: Entities::PipelineBasic + end + + desc 'Create a new pipeline' do + detail 'This feature was introduced in GitLab 8.14' + success Entities::Pipeline + end + params do + requires :ref, type: String, desc: 'Reference' + optional :variables, Array, desc: 'Array of variables available in the pipeline' + end + post ':id/pipeline' do + Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42124') + + authorize! :create_pipeline, user_project + + pipeline_params = declared_params(include_missing: false) + .merge(variables_attributes: params[:variables]) + .except(:variables) + + new_pipeline = ::Ci::CreatePipelineService.new(user_project, + current_user, + pipeline_params) + .execute(:api, ignore_skip_ci: true, save_on_errors: false) + + if new_pipeline.persisted? + present new_pipeline, with: Entities::Pipeline + else + render_validation_error!(new_pipeline) + end + end + + desc 'Gets a the latest pipeline for the project branch' do + detail 'This feature was introduced in GitLab 12.3' + success Entities::Pipeline + end + params do + optional :ref, type: String, desc: 'branch ref of pipeline' + end + get ':id/pipelines/latest' do + authorize! :read_pipeline, latest_pipeline + + present latest_pipeline, with: Entities::Pipeline + end + + desc 'Gets a specific pipeline for the project' do + detail 'This feature was introduced in GitLab 8.11' + success Entities::Pipeline + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + end + get ':id/pipelines/:pipeline_id' do + authorize! :read_pipeline, pipeline + + present pipeline, with: Entities::Pipeline + end + + desc 'Gets the variables for a given pipeline' do + detail 'This feature was introduced in GitLab 11.11' + success Entities::Variable + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + end + get ':id/pipelines/:pipeline_id/variables' do + authorize! :read_pipeline_variable, pipeline + + present pipeline.variables, with: Entities::Variable + end + + desc 'Gets the test report for a given pipeline' do + detail 'This feature was introduced in GitLab 13.0. Disabled by default behind feature flag `junit_pipeline_view`' + success TestReportEntity + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + end + get ':id/pipelines/:pipeline_id/test_report' do + not_found! unless Feature.enabled?(:junit_pipeline_view, user_project) + + authorize! :read_build, pipeline + + present pipeline.test_reports, with: TestReportEntity, details: true + end + + desc 'Deletes a pipeline' do + detail 'This feature was introduced in GitLab 11.6' + http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']] + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + end + delete ':id/pipelines/:pipeline_id' do + authorize! :destroy_pipeline, pipeline + + destroy_conditionally!(pipeline) do + ::Ci::DestroyPipelineService.new(user_project, current_user).execute(pipeline) + end + end + + desc 'Retry builds in the pipeline' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Pipeline + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + end + post ':id/pipelines/:pipeline_id/retry' do + authorize! :update_pipeline, pipeline + + pipeline.retry_failed(current_user) + + present pipeline, with: Entities::Pipeline + end + + desc 'Cancel all builds in the pipeline' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Pipeline + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + end + post ':id/pipelines/:pipeline_id/cancel' do + authorize! :update_pipeline, pipeline + + pipeline.cancel_running + + status 200 + present pipeline.reset, with: Entities::Pipeline + end + end + + helpers do + def pipeline + strong_memoize(:pipeline) do + user_project.ci_pipelines.find(params[:pipeline_id]) + end + end + + def latest_pipeline + strong_memoize(:latest_pipeline) do + user_project.latest_pipeline_for_ref(params[:ref]) + end + end + end + end + end +end diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb deleted file mode 100644 index 9af16f61967..00000000000 --- a/lib/api/pipeline_schedules.rb +++ /dev/null @@ -1,215 +0,0 @@ -# frozen_string_literal: true - -module API - class PipelineSchedules < Grape::API::Instance - include PaginationParams - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc 'Get all pipeline schedules' do - success Entities::PipelineSchedule - end - params do - use :pagination - optional :scope, type: String, values: %w[active inactive], - desc: 'The scope of pipeline schedules' - end - # rubocop: disable CodeReuse/ActiveRecord - get ':id/pipeline_schedules' do - authorize! :read_pipeline_schedule, user_project - - schedules = ::Ci::PipelineSchedulesFinder.new(user_project).execute(scope: params[:scope]) - .preload([:owner, :last_pipeline]) - present paginate(schedules), with: Entities::PipelineSchedule - end - # rubocop: enable CodeReuse/ActiveRecord - - desc 'Get a single pipeline schedule' do - success Entities::PipelineScheduleDetails - end - params do - requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' - end - get ':id/pipeline_schedules/:pipeline_schedule_id' do - present pipeline_schedule, with: Entities::PipelineScheduleDetails - end - - desc 'Create a new pipeline schedule' do - success Entities::PipelineScheduleDetails - end - params do - requires :description, type: String, desc: 'The description of pipeline schedule' - requires :ref, type: String, desc: 'The branch/tag name will be triggered', allow_blank: false - requires :cron, type: String, desc: 'The cron' - optional :cron_timezone, type: String, default: 'UTC', desc: 'The timezone' - optional :active, type: Boolean, default: true, desc: 'The activation of pipeline schedule' - end - post ':id/pipeline_schedules' do - authorize! :create_pipeline_schedule, user_project - - pipeline_schedule = ::Ci::CreatePipelineScheduleService - .new(user_project, current_user, declared_params(include_missing: false)) - .execute - - if pipeline_schedule.persisted? - present pipeline_schedule, with: Entities::PipelineScheduleDetails - else - render_validation_error!(pipeline_schedule) - end - end - - desc 'Edit a pipeline schedule' do - success Entities::PipelineScheduleDetails - end - params do - requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' - optional :description, type: String, desc: 'The description of pipeline schedule' - optional :ref, type: String, desc: 'The branch/tag name will be triggered' - optional :cron, type: String, desc: 'The cron' - optional :cron_timezone, type: String, desc: 'The timezone' - optional :active, type: Boolean, desc: 'The activation of pipeline schedule' - end - put ':id/pipeline_schedules/:pipeline_schedule_id' do - authorize! :update_pipeline_schedule, pipeline_schedule - - if pipeline_schedule.update(declared_params(include_missing: false)) - present pipeline_schedule, with: Entities::PipelineScheduleDetails - else - render_validation_error!(pipeline_schedule) - end - end - - desc 'Take ownership of a pipeline schedule' do - success Entities::PipelineScheduleDetails - end - params do - requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' - end - post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do - authorize! :update_pipeline_schedule, pipeline_schedule - - if pipeline_schedule.own!(current_user) - present pipeline_schedule, with: Entities::PipelineScheduleDetails - else - render_validation_error!(pipeline_schedule) - end - end - - desc 'Delete a pipeline schedule' do - success Entities::PipelineScheduleDetails - end - params do - requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' - end - delete ':id/pipeline_schedules/:pipeline_schedule_id' do - authorize! :admin_pipeline_schedule, pipeline_schedule - - destroy_conditionally!(pipeline_schedule) - end - - desc 'Play a scheduled pipeline immediately' do - detail 'This feature was added in GitLab 12.8' - end - params do - requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' - end - post ':id/pipeline_schedules/:pipeline_schedule_id/play' do - authorize! :play_pipeline_schedule, pipeline_schedule - - job_id = RunPipelineScheduleWorker # rubocop:disable CodeReuse/Worker - .perform_async(pipeline_schedule.id, current_user.id) - - if job_id - created! - else - render_api_error!('Unable to schedule pipeline run immediately', 500) - end - end - - desc 'Create a new pipeline schedule variable' do - success Entities::Variable - end - params do - requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' - requires :key, type: String, desc: 'The key of the variable' - requires :value, type: String, desc: 'The value of the variable' - optional :variable_type, type: String, values: ::Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var' - end - post ':id/pipeline_schedules/:pipeline_schedule_id/variables' do - authorize! :update_pipeline_schedule, pipeline_schedule - - variable_params = declared_params(include_missing: false) - variable = pipeline_schedule.variables.create(variable_params) - if variable.persisted? - present variable, with: Entities::Variable - else - render_validation_error!(variable) - end - end - - desc 'Edit a pipeline schedule variable' do - success Entities::Variable - end - params do - requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' - requires :key, type: String, desc: 'The key of the variable' - optional :value, type: String, desc: 'The value of the variable' - optional :variable_type, type: String, values: ::Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file' - end - put ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do - authorize! :update_pipeline_schedule, pipeline_schedule - - if pipeline_schedule_variable.update(declared_params(include_missing: false)) - present pipeline_schedule_variable, with: Entities::Variable - else - render_validation_error!(pipeline_schedule_variable) - end - end - - desc 'Delete a pipeline schedule variable' do - success Entities::Variable - end - params do - requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' - requires :key, type: String, desc: 'The key of the variable' - end - delete ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do - authorize! :admin_pipeline_schedule, pipeline_schedule - - status :accepted - present pipeline_schedule_variable.destroy, with: Entities::Variable - end - end - - helpers do - # rubocop: disable CodeReuse/ActiveRecord - def pipeline_schedule - @pipeline_schedule ||= - user_project - .pipeline_schedules - .preload(:owner, :last_pipeline) - .find_by(id: params.delete(:pipeline_schedule_id)).tap do |pipeline_schedule| - unless can?(current_user, :read_pipeline_schedule, pipeline_schedule) - not_found!('Pipeline Schedule') - end - end - end - # rubocop: enable CodeReuse/ActiveRecord - - # rubocop: disable CodeReuse/ActiveRecord - def pipeline_schedule_variable - @pipeline_schedule_variable ||= - pipeline_schedule.variables.find_by(key: params[:key]).tap do |pipeline_schedule_variable| - unless pipeline_schedule_variable - not_found!('Pipeline Schedule Variable') - end - end - end - # rubocop: enable CodeReuse/ActiveRecord - end - end -end diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb deleted file mode 100644 index 1aac7b7deb4..00000000000 --- a/lib/api/pipelines.rb +++ /dev/null @@ -1,187 +0,0 @@ -# frozen_string_literal: true - -module API - class Pipelines < Grape::API::Instance - include PaginationParams - - before { authenticate_non_get! } - - params do - requires :id, type: String, desc: 'The project ID' - end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc 'Get all Pipelines of the project' do - detail 'This feature was introduced in GitLab 8.11.' - success Entities::PipelineBasic - end - params do - use :pagination - optional :scope, type: String, values: %w[running pending finished branches tags], - desc: 'The scope of pipelines' - optional :status, type: String, values: ::Ci::HasStatus::AVAILABLE_STATUSES, - desc: 'The status of pipelines' - optional :ref, type: String, desc: 'The ref of pipelines' - optional :sha, type: String, desc: 'The sha of pipelines' - optional :yaml_errors, type: Boolean, desc: 'Returns pipelines with invalid configurations' - optional :name, type: String, desc: 'The name of the user who triggered pipelines' - optional :username, type: String, desc: 'The username of the user who triggered pipelines' - optional :updated_before, type: DateTime, desc: 'Return pipelines updated before the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ' - optional :updated_after, type: DateTime, desc: 'Return pipelines updated after the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ' - optional :order_by, type: String, values: ::Ci::PipelinesFinder::ALLOWED_INDEXED_COLUMNS, default: 'id', - desc: 'Order pipelines' - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Sort pipelines' - end - get ':id/pipelines' do - authorize! :read_pipeline, user_project - authorize! :read_build, user_project - - pipelines = ::Ci::PipelinesFinder.new(user_project, current_user, params).execute - present paginate(pipelines), with: Entities::PipelineBasic - end - - desc 'Create a new pipeline' do - detail 'This feature was introduced in GitLab 8.14' - success Entities::Pipeline - end - params do - requires :ref, type: String, desc: 'Reference' - optional :variables, Array, desc: 'Array of variables available in the pipeline' - end - post ':id/pipeline' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42124') - - authorize! :create_pipeline, user_project - - pipeline_params = declared_params(include_missing: false) - .merge(variables_attributes: params[:variables]) - .except(:variables) - - new_pipeline = ::Ci::CreatePipelineService.new(user_project, - current_user, - pipeline_params) - .execute(:api, ignore_skip_ci: true, save_on_errors: false) - - if new_pipeline.persisted? - present new_pipeline, with: Entities::Pipeline - else - render_validation_error!(new_pipeline) - end - end - - desc 'Gets a the latest pipeline for the project branch' do - detail 'This feature was introduced in GitLab 12.3' - success Entities::Pipeline - end - params do - optional :ref, type: String, desc: 'branch ref of pipeline' - end - get ':id/pipelines/latest' do - authorize! :read_pipeline, latest_pipeline - - present latest_pipeline, with: Entities::Pipeline - end - - desc 'Gets a specific pipeline for the project' do - detail 'This feature was introduced in GitLab 8.11' - success Entities::Pipeline - end - params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' - end - get ':id/pipelines/:pipeline_id' do - authorize! :read_pipeline, pipeline - - present pipeline, with: Entities::Pipeline - end - - desc 'Gets the variables for a given pipeline' do - detail 'This feature was introduced in GitLab 11.11' - success Entities::Variable - end - params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' - end - get ':id/pipelines/:pipeline_id/variables' do - authorize! :read_pipeline_variable, pipeline - - present pipeline.variables, with: Entities::Variable - end - - desc 'Gets the test report for a given pipeline' do - detail 'This feature was introduced in GitLab 13.0. Disabled by default behind feature flag `junit_pipeline_view`' - success TestReportEntity - end - params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' - end - get ':id/pipelines/:pipeline_id/test_report' do - not_found! unless Feature.enabled?(:junit_pipeline_view, user_project) - - authorize! :read_build, pipeline - - present pipeline.test_reports, with: TestReportEntity, details: true - end - - desc 'Deletes a pipeline' do - detail 'This feature was introduced in GitLab 11.6' - http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']] - end - params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' - end - delete ':id/pipelines/:pipeline_id' do - authorize! :destroy_pipeline, pipeline - - destroy_conditionally!(pipeline) do - ::Ci::DestroyPipelineService.new(user_project, current_user).execute(pipeline) - end - end - - desc 'Retry builds in the pipeline' do - detail 'This feature was introduced in GitLab 8.11.' - success Entities::Pipeline - end - params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' - end - post ':id/pipelines/:pipeline_id/retry' do - authorize! :update_pipeline, pipeline - - pipeline.retry_failed(current_user) - - present pipeline, with: Entities::Pipeline - end - - desc 'Cancel all builds in the pipeline' do - detail 'This feature was introduced in GitLab 8.11.' - success Entities::Pipeline - end - params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' - end - post ':id/pipelines/:pipeline_id/cancel' do - authorize! :update_pipeline, pipeline - - pipeline.cancel_running - - status 200 - present pipeline.reset, with: Entities::Pipeline - end - end - - helpers do - def pipeline - strong_memoize(:pipeline) do - user_project.ci_pipelines.find(params[:pipeline_id]) - end - end - - def latest_pipeline - strong_memoize(:latest_pipeline) do - user_project.latest_pipeline_for_ref(params[:ref]) - end - end - end - end -end diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb index a30b6c6ef0e..769d0dffd0b 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb @@ -19,7 +19,7 @@ module Gitlab end unless allowed_to_write_ref? - return error("Insufficient permissions for protected ref '#{command.ref}'") + error("Insufficient permissions for protected ref '#{command.ref}'") end end diff --git a/lib/gitlab/ci/pipeline/chain/validate/repository.rb b/lib/gitlab/ci/pipeline/chain/validate/repository.rb index 8f5445850d7..7977ce90443 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/repository.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/repository.rb @@ -18,7 +18,7 @@ module Gitlab end if @command.ambiguous_ref? - return error('Ref is ambiguous') + error('Ref is ambiguous') end end diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb index dfff6823689..1330b06bf9c 100644 --- a/lib/gitlab/git_ref_validator.rb +++ b/lib/gitlab/git_ref_validator.rb @@ -19,7 +19,7 @@ module Gitlab begin Rugged::Reference.valid_name?("refs/heads/#{ref_name}") rescue ArgumentError - return false + false end end @@ -35,7 +35,7 @@ module Gitlab begin Rugged::Reference.valid_name?(expanded_name) rescue ArgumentError - return false + false end end end diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 5b119fe616a..20ad6d0184b 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -201,9 +201,9 @@ module Gitlab response = GitalyClient.call(@storage, :repository_service, :fsck, request, timeout: GitalyClient.long_timeout) if response.error.empty? - return "", 0 + ["", 0] else - return response.error.b, 1 + [response.error.b, 1] end end diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index abdbccd3aa8..47d0b9ba8cb 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -101,7 +101,7 @@ module Gitlab if project # If a project is found and the user has access, we return the full project path - return project.full_path, project.default_branch + [project.full_path, project.default_branch] else # If not, we return the first two components as if it were a simple `namespace/project` path, # so that we don't reveal the existence of a nested project the user doesn't have access to. @@ -112,7 +112,7 @@ module Gitlab # `go get gitlab.com/group/subgroup/project/subpackage` will not work for private projects. # `go get gitlab.com/group/subgroup/project.git/subpackage` will work, since Go is smart enough # to figure that out. `import 'gitlab.com/...'` behaves the same as `go get`. - return simple_project_path, 'master' + [simple_project_path, 'master'] end end diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index e80cc51dc3b..5dfe8fc7ae3 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -56,7 +56,7 @@ module Gitlab # * Maximum length is 63 bytes # * First/Last Character is not a hyphen def slugify(str) - return str.downcase + str.downcase .gsub(/[^a-z0-9]/, '-')[0..62] .gsub(/(\A-+|-+\z)/, '') end diff --git a/lib/google_api/auth.rb b/lib/google_api/auth.rb index 319e5d2063c..7d9ff579c92 100644 --- a/lib/google_api/auth.rb +++ b/lib/google_api/auth.rb @@ -22,7 +22,7 @@ module GoogleApi def get_token(code) ret = client.auth_code.get_token(code, redirect_uri: redirect_uri) - return ret.token, ret.expires_at + [ret.token, ret.expires_at] end protected diff --git a/spec/requests/api/ci/pipeline_schedules_spec.rb b/spec/requests/api/ci/pipeline_schedules_spec.rb new file mode 100644 index 00000000000..e0199b7b51c --- /dev/null +++ b/spec/requests/api/ci/pipeline_schedules_spec.rb @@ -0,0 +1,522 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::PipelineSchedules do + let_it_be(:developer) { create(:user) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, public_builds: false) } + + before do + project.add_developer(developer) + end + + describe 'GET /projects/:id/pipeline_schedules' do + context 'authenticated user with valid permissions' do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) } + + before do + pipeline_schedule.pipelines << build(:ci_pipeline, project: project) + end + + def create_pipeline_schedules(count) + create_list(:ci_pipeline_schedule, count, project: project) + .each do |pipeline_schedule| + create(:user).tap do |user| + project.add_developer(user) + pipeline_schedule.update!(owner: user) + end + pipeline_schedule.pipelines << build(:ci_pipeline, project: project) + end + end + + it 'returns list of pipeline_schedules' do + get api("/projects/#{project.id}/pipeline_schedules", developer) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('pipeline_schedules') + end + + it 'avoids N + 1 queries' do + # We need at least two users to trigger a preload for that relation. + create_pipeline_schedules(1) + + control_count = ActiveRecord::QueryRecorder.new do + get api("/projects/#{project.id}/pipeline_schedules", developer) + end.count + + create_pipeline_schedules(5) + + expect do + get api("/projects/#{project.id}/pipeline_schedules", developer) + end.not_to exceed_query_limit(control_count) + end + + %w[active inactive].each do |target| + context "when scope is #{target}" do + before do + create(:ci_pipeline_schedule, project: project, active: active?(target)) + end + + it 'returns matched pipeline schedules' do + get api("/projects/#{project.id}/pipeline_schedules", developer), params: { scope: target } + + expect(json_response.map { |r| r['active'] }).to all(eq(active?(target))) + end + end + + def active?(str) + str == 'active' + end + end + end + + context 'authenticated user with invalid permissions' do + it 'does not return pipeline_schedules list' do + get api("/projects/#{project.id}/pipeline_schedules", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not return pipeline_schedules list' do + get api("/projects/#{project.id}/pipeline_schedules") + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe 'GET /projects/:id/pipeline_schedules/:pipeline_schedule_id' do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) } + + before do + pipeline_schedule.variables << build(:ci_pipeline_schedule_variable) + pipeline_schedule.pipelines << build(:ci_pipeline, project: project) + end + + context 'authenticated user with valid permissions' do + it 'returns pipeline_schedule details' do + get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('pipeline_schedule') + end + + it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do + get api("/projects/#{project.id}/pipeline_schedules/-5", developer) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not return pipeline_schedules list' do + get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'authenticated user with insufficient permissions' do + before do + project.add_guest(user) + end + + it 'does not return pipeline_schedules list' do + get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not return pipeline_schedules list' do + get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe 'POST /projects/:id/pipeline_schedules' do + let(:params) { attributes_for(:ci_pipeline_schedule) } + + context 'authenticated user with valid permissions' do + context 'with required parameters' do + it 'creates pipeline_schedule' do + expect do + post api("/projects/#{project.id}/pipeline_schedules", developer), + params: params + end.to change { project.pipeline_schedules.count }.by(1) + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema('pipeline_schedule') + expect(json_response['description']).to eq(params[:description]) + expect(json_response['ref']).to eq(params[:ref]) + expect(json_response['cron']).to eq(params[:cron]) + expect(json_response['cron_timezone']).to eq(params[:cron_timezone]) + expect(json_response['owner']['id']).to eq(developer.id) + end + end + + context 'without required parameters' do + it 'does not create pipeline_schedule' do + post api("/projects/#{project.id}/pipeline_schedules", developer) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when cron has validation error' do + it 'does not create pipeline_schedule' do + post api("/projects/#{project.id}/pipeline_schedules", developer), + params: params.merge('cron' => 'invalid-cron') + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to have_key('cron') + end + end + end + + context 'authenticated user with invalid permissions' do + it 'does not create pipeline_schedule' do + post api("/projects/#{project.id}/pipeline_schedules", user), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not create pipeline_schedule' do + post api("/projects/#{project.id}/pipeline_schedules"), params: params + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' do + let(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + context 'authenticated user with valid permissions' do + it 'updates cron' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer), + params: { cron: '1 2 3 4 *' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('pipeline_schedule') + expect(json_response['cron']).to eq('1 2 3 4 *') + end + + context 'when cron has validation error' do + it 'does not update pipeline_schedule' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer), + params: { cron: 'invalid-cron' } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to have_key('cron') + end + end + end + + context 'authenticated user with invalid permissions' do + it 'does not update pipeline_schedule' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not update pipeline_schedule' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do + let(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + context 'authenticated user with valid permissions' do + it 'updates owner' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", developer) + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema('pipeline_schedule') + end + end + + context 'authenticated user with invalid permissions' do + it 'does not update owner' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not update owner' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership") + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id' do + let(:maintainer) { create(:user) } + + let!(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + before do + project.add_maintainer(maintainer) + end + + context 'authenticated user with valid permissions' do + it 'deletes pipeline_schedule' do + expect do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", maintainer) + end.to change { project.pipeline_schedules.count }.by(-1) + + expect(response).to have_gitlab_http_status(:no_content) + end + + it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do + delete api("/projects/#{project.id}/pipeline_schedules/-5", maintainer) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it_behaves_like '412 response' do + let(:request) { api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", maintainer) } + end + end + + context 'authenticated user with invalid permissions' do + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: maintainer) } + + it 'does not delete pipeline_schedule' do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'unauthenticated user' do + it 'does not delete pipeline_schedule' do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/play' do + let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } + + let(:route) { ->(id) { "/projects/#{project.id}/pipeline_schedules/#{id}/play" } } + + context 'authenticated user with `:play_pipeline_schedule` permission' do + it 'schedules a pipeline worker' do + project.add_developer(developer) + + expect(RunPipelineScheduleWorker) + .to receive(:perform_async) + .with(pipeline_schedule.id, developer.id) + .and_call_original + post api(route[pipeline_schedule.id], developer) + + expect(response).to have_gitlab_http_status(:created) + end + + it 'renders an error if scheduling failed' do + project.add_developer(developer) + + expect(RunPipelineScheduleWorker) + .to receive(:perform_async) + .with(pipeline_schedule.id, developer.id) + .and_return(nil) + post api(route[pipeline_schedule.id], developer) + + expect(response).to have_gitlab_http_status(:internal_server_error) + end + end + + context 'authenticated user with insufficient access' do + it 'responds with not found' do + project.add_guest(user) + + post api(route[pipeline_schedule.id], user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'responds with unauthorized' do + post api(route[pipeline_schedule.id]) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables' do + let(:params) { attributes_for(:ci_pipeline_schedule_variable) } + + let_it_be(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + context 'authenticated user with valid permissions' do + context 'with required parameters' do + it 'creates pipeline_schedule_variable' do + expect do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer), + params: params.merge(variable_type: 'file') + end.to change { pipeline_schedule.variables.count }.by(1) + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema('pipeline_schedule_variable') + expect(json_response['key']).to eq(params[:key]) + expect(json_response['value']).to eq(params[:value]) + expect(json_response['variable_type']).to eq('file') + end + end + + context 'without required parameters' do + it 'does not create pipeline_schedule_variable' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when key has validation error' do + it 'does not create pipeline_schedule_variable' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer), + params: params.merge('key' => '!?!?') + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to have_key('key') + end + end + end + + context 'authenticated user with invalid permissions' do + it 'does not create pipeline_schedule_variable' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", user), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not create pipeline_schedule_variable' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables"), params: params + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do + let_it_be(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + let(:pipeline_schedule_variable) do + create(:ci_pipeline_schedule_variable, pipeline_schedule: pipeline_schedule) + end + + context 'authenticated user with valid permissions' do + it 'updates pipeline_schedule_variable' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer), + params: { value: 'updated_value', variable_type: 'file' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('pipeline_schedule_variable') + expect(json_response['value']).to eq('updated_value') + expect(json_response['variable_type']).to eq('file') + end + end + + context 'authenticated user with invalid permissions' do + it 'does not update pipeline_schedule_variable' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not update pipeline_schedule_variable' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}") + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do + let(:maintainer) { create(:user) } + + let_it_be(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + let!(:pipeline_schedule_variable) do + create(:ci_pipeline_schedule_variable, pipeline_schedule: pipeline_schedule) + end + + before do + project.add_maintainer(maintainer) + end + + context 'authenticated user with valid permissions' do + it 'deletes pipeline_schedule_variable' do + expect do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", maintainer) + end.to change { Ci::PipelineScheduleVariable.count }.by(-1) + + expect(response).to have_gitlab_http_status(:accepted) + expect(response).to match_response_schema('pipeline_schedule_variable') + end + + it 'responds with 404 Not Found if requesting non-existing pipeline_schedule_variable' do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/____", maintainer) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'authenticated user with invalid permissions' do + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: maintainer) } + + it 'does not delete pipeline_schedule_variable' do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'unauthenticated user' do + it 'does not delete pipeline_schedule_variable' do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}") + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end +end diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb new file mode 100644 index 00000000000..c9ca806e2c4 --- /dev/null +++ b/spec/requests/api/ci/pipelines_spec.rb @@ -0,0 +1,786 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Pipelines do + let_it_be(:user) { create(:user) } + let_it_be(:non_member) { create(:user) } + + # We need to reload as the shared example 'pipelines visibility table' is changing project + let_it_be(:project, reload: true) do + create(:project, :repository, creator: user) + end + + let_it_be(:pipeline) do + create(:ci_empty_pipeline, project: project, sha: project.commit.id, + ref: project.default_branch, user: user) + end + + before do + project.add_maintainer(user) + end + + describe 'GET /projects/:id/pipelines ' do + it_behaves_like 'pipelines visibility table' + + context 'authorized user' do + it 'returns project pipelines' do + get api("/projects/#{project.id}/pipelines", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['sha']).to match(/\A\h{40}\z/) + expect(json_response.first['id']).to eq pipeline.id + expect(json_response.first['web_url']).to be_present + expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status web_url created_at updated_at]) + end + + context 'when parameter is passed' do + %w[running pending].each do |target| + context "when scope is #{target}" do + before do + create(:ci_pipeline, project: project, status: target) + end + + it 'returns matched pipelines' do + get api("/projects/#{project.id}/pipelines", user), params: { scope: target } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).not_to be_empty + json_response.each { |r| expect(r['status']).to eq(target) } + end + end + end + + context 'when scope is finished' do + before do + create(:ci_pipeline, project: project, status: 'success') + create(:ci_pipeline, project: project, status: 'failed') + create(:ci_pipeline, project: project, status: 'canceled') + end + + it 'returns matched pipelines' do + get api("/projects/#{project.id}/pipelines", user), params: { scope: 'finished' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).not_to be_empty + json_response.each { |r| expect(r['status']).to be_in(%w[success failed canceled]) } + end + end + + context 'when scope is branches or tags' do + let_it_be(:pipeline_branch) { create(:ci_pipeline, project: project) } + let_it_be(:pipeline_tag) { create(:ci_pipeline, project: project, ref: 'v1.0.0', tag: true) } + + context 'when scope is branches' do + it 'returns matched pipelines' do + get api("/projects/#{project.id}/pipelines", user), params: { scope: 'branches' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).not_to be_empty + expect(json_response.last['id']).to eq(pipeline_branch.id) + end + end + + context 'when scope is tags' do + it 'returns matched pipelines' do + get api("/projects/#{project.id}/pipelines", user), params: { scope: 'tags' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).not_to be_empty + expect(json_response.last['id']).to eq(pipeline_tag.id) + end + end + end + + context 'when scope is invalid' do + it 'returns bad_request' do + get api("/projects/#{project.id}/pipelines", user), params: { scope: 'invalid-scope' } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + Ci::HasStatus::AVAILABLE_STATUSES.each do |target| + context "when status is #{target}" do + before do + create(:ci_pipeline, project: project, status: target) + exception_status = Ci::HasStatus::AVAILABLE_STATUSES - [target] + create(:ci_pipeline, project: project, status: exception_status.sample) + end + + it 'returns matched pipelines' do + get api("/projects/#{project.id}/pipelines", user), params: { status: target } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).not_to be_empty + json_response.each { |r| expect(r['status']).to eq(target) } + end + end + end + + context 'when status is invalid' do + it 'returns bad_request' do + get api("/projects/#{project.id}/pipelines", user), params: { status: 'invalid-status' } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when ref is specified' do + before do + create(:ci_pipeline, project: project) + end + + context 'when ref exists' do + it 'returns matched pipelines' do + get api("/projects/#{project.id}/pipelines", user), params: { ref: 'master' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).not_to be_empty + json_response.each { |r| expect(r['ref']).to eq('master') } + end + end + + context 'when ref does not exist' do + it 'returns empty' do + get api("/projects/#{project.id}/pipelines", user), params: { ref: 'invalid-ref' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_empty + end + end + end + + context 'when name is specified' do + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + + context 'when name exists' do + it 'returns matched pipelines' do + get api("/projects/#{project.id}/pipelines", user), params: { name: user.name } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.first['id']).to eq(pipeline.id) + end + end + + context 'when name does not exist' do + it 'returns empty' do + get api("/projects/#{project.id}/pipelines", user), params: { name: 'invalid-name' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_empty + end + end + end + + context 'when username is specified' do + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + + context 'when username exists' do + it 'returns matched pipelines' do + get api("/projects/#{project.id}/pipelines", user), params: { username: user.username } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.first['id']).to eq(pipeline.id) + end + end + + context 'when username does not exist' do + it 'returns empty' do + get api("/projects/#{project.id}/pipelines", user), params: { username: 'invalid-username' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_empty + end + end + end + + context 'when yaml_errors is specified' do + let_it_be(:pipeline1) { create(:ci_pipeline, project: project, yaml_errors: 'Syntax error') } + let_it_be(:pipeline2) { create(:ci_pipeline, project: project) } + + context 'when yaml_errors is true' do + it 'returns matched pipelines' do + get api("/projects/#{project.id}/pipelines", user), params: { yaml_errors: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.first['id']).to eq(pipeline1.id) + end + end + + context 'when yaml_errors is false' do + it 'returns matched pipelines' do + get api("/projects/#{project.id}/pipelines", user), params: { yaml_errors: false } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.first['id']).to eq(pipeline2.id) + end + end + + context 'when yaml_errors is invalid' do + it 'returns bad_request' do + get api("/projects/#{project.id}/pipelines", user), params: { yaml_errors: 'invalid-yaml_errors' } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + + context 'when updated_at filters are specified' do + let_it_be(:pipeline1) { create(:ci_pipeline, project: project, updated_at: 2.days.ago) } + let_it_be(:pipeline2) { create(:ci_pipeline, project: project, updated_at: 4.days.ago) } + let_it_be(:pipeline3) { create(:ci_pipeline, project: project, updated_at: 1.hour.ago) } + + it 'returns pipelines with last update date in specified datetime range' do + get api("/projects/#{project.id}/pipelines", user), params: { updated_before: 1.day.ago, updated_after: 3.days.ago } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.first['id']).to eq(pipeline1.id) + end + end + + context 'when order_by and sort are specified' do + context 'when order_by user_id' do + before do + create_list(:user, 3).each do |some_user| + create(:ci_pipeline, project: project, user: some_user) + end + end + + context 'when sort parameter is valid' do + it 'sorts as user_id: :desc' do + get api("/projects/#{project.id}/pipelines", user), params: { order_by: 'user_id', sort: 'desc' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).not_to be_empty + + pipeline_ids = Ci::Pipeline.all.order(user_id: :desc).pluck(:id) + expect(json_response.map { |r| r['id'] }).to eq(pipeline_ids) + end + end + + context 'when sort parameter is invalid' do + it 'returns bad_request' do + get api("/projects/#{project.id}/pipelines", user), params: { order_by: 'user_id', sort: 'invalid_sort' } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + + context 'when order_by is invalid' do + it 'returns bad_request' do + get api("/projects/#{project.id}/pipelines", user), params: { order_by: 'lock_version', sort: 'asc' } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + end + end + + context 'unauthorized user' do + it 'does not return project pipelines' do + get api("/projects/#{project.id}/pipelines", non_member) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response).not_to be_an Array + end + end + end + + describe 'POST /projects/:id/pipeline ' do + def expect_variables(variables, expected_variables) + variables.each_with_index do |variable, index| + expected_variable = expected_variables[index] + + expect(variable.key).to eq(expected_variable['key']) + expect(variable.value).to eq(expected_variable['value']) + expect(variable.variable_type).to eq(expected_variable['variable_type']) + end + end + + context 'authorized user' do + context 'with gitlab-ci.yml' do + before do + stub_ci_pipeline_to_return_yaml_file + end + + it 'creates and returns a new pipeline' do + expect do + post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch } + end.to change { project.ci_pipelines.count }.by(1) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to be_a Hash + expect(json_response['sha']).to eq project.commit.id + end + + context 'variables given' do + let(:variables) { [{ 'variable_type' => 'file', 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] } + + it 'creates and returns a new pipeline using the given variables' do + expect do + post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch, variables: variables } + end.to change { project.ci_pipelines.count }.by(1) + expect_variables(project.ci_pipelines.last.variables, variables) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to be_a Hash + expect(json_response['sha']).to eq project.commit.id + expect(json_response).not_to have_key('variables') + end + end + + describe 'using variables conditions' do + let(:variables) { [{ 'variable_type' => 'env_var', 'key' => 'STAGING', 'value' => 'true' }] } + + before do + config = YAML.dump(test: { script: 'test', only: { variables: ['$STAGING'] } }) + stub_ci_pipeline_yaml_file(config) + end + + it 'creates and returns a new pipeline using the given variables' do + expect do + post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch, variables: variables } + end.to change { project.ci_pipelines.count }.by(1) + expect_variables(project.ci_pipelines.last.variables, variables) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to be_a Hash + expect(json_response['sha']).to eq project.commit.id + expect(json_response).not_to have_key('variables') + end + + context 'condition unmatch' do + let(:variables) { [{ 'key' => 'STAGING', 'value' => 'false' }] } + + it "doesn't create a job" do + expect do + post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch } + end.not_to change { project.ci_pipelines.count } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + + it 'fails when using an invalid ref' do + post api("/projects/#{project.id}/pipeline", user), params: { ref: 'invalid_ref' } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['base'].first).to eq 'Reference not found' + expect(json_response).not_to be_an Array + end + end + + context 'without gitlab-ci.yml' do + context 'without auto devops enabled' do + before do + project.update!(auto_devops_attributes: { enabled: false }) + end + + it 'fails to create pipeline' do + post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['base'].first).to eq 'Missing CI config file' + expect(json_response).not_to be_an Array + end + end + end + end + + context 'unauthorized user' do + it 'does not create pipeline' do + post api("/projects/#{project.id}/pipeline", non_member), params: { ref: project.default_branch } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response).not_to be_an Array + end + end + end + + describe 'GET /projects/:id/pipelines/:pipeline_id' do + it_behaves_like 'pipelines visibility table' do + let(:pipelines_api_path) do + "/projects/#{project.id}/pipelines/#{pipeline.id}" + end + + let(:api_response) { response_status == 200 ? response : json_response } + let(:response_200) { match_response_schema('public_api/v4/pipeline/detail') } + end + + context 'authorized user' do + it 'exposes known attributes' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/pipeline/detail') + end + + it 'returns project pipelines' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['sha']).to match(/\A\h{40}\z/) + end + + it 'returns 404 when it does not exist' do + get api("/projects/#{project.id}/pipelines/#{non_existing_record_id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq '404 Not found' + expect(json_response['id']).to be nil + end + + context 'with coverage' do + before do + create(:ci_build, coverage: 30, pipeline: pipeline) + end + + it 'exposes the coverage' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) + + expect(json_response["coverage"].to_i).to eq(30) + end + end + end + + context 'unauthorized user' do + it 'does not return a project pipeline' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response['id']).to be nil + end + end + end + + describe 'GET /projects/:id/pipelines/latest' do + context 'authorized user' do + let(:second_branch) { project.repository.branches[2] } + + let!(:second_pipeline) do + create(:ci_empty_pipeline, project: project, sha: second_branch.target, + ref: second_branch.name, user: user) + end + + before do + create(:ci_empty_pipeline, project: project, sha: project.commit.parent.id, + ref: project.default_branch, user: user) + end + + context 'default repository branch' do + it 'gets the latest pipleine' do + get api("/projects/#{project.id}/pipelines/latest", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/pipeline/detail') + expect(json_response['ref']).to eq(project.default_branch) + expect(json_response['sha']).to eq(project.commit.id) + end + end + + context 'ref parameter' do + it 'gets the latest pipleine' do + get api("/projects/#{project.id}/pipelines/latest", user), params: { ref: second_branch.name } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/pipeline/detail') + expect(json_response['ref']).to eq(second_branch.name) + expect(json_response['sha']).to eq(second_branch.target) + end + end + end + + context 'unauthorized user' do + it 'does not return a project pipeline' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response['id']).to be nil + end + end + end + + describe 'GET /projects/:id/pipelines/:pipeline_id/variables' do + subject { get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", api_user) } + + let(:api_user) { user } + + context 'user is a mantainer' do + it 'returns pipeline variables empty' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_empty + end + + context 'with variables' do + let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') } + + it 'returns pipeline variables' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to contain_exactly({ "variable_type" => "env_var", "key" => "foo", "value" => "bar" }) + end + end + end + + context 'user is a developer' do + let(:pipeline_owner_user) { create(:user) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, user: pipeline_owner_user) } + + before do + project.add_developer(api_user) + end + + context 'pipeline created by the developer user' do + let(:api_user) { pipeline_owner_user } + let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') } + + it 'returns pipeline variables' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to contain_exactly({ "variable_type" => "env_var", "key" => "foo", "value" => "bar" }) + end + end + + context 'pipeline created is not created by the developer user' do + let(:api_user) { create(:user) } + + it 'does not return pipeline variables' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'user is not a project member' do + it 'does not return pipeline variables' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", non_member) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq '404 Project Not Found' + end + end + end + + describe 'DELETE /projects/:id/pipelines/:pipeline_id' do + context 'authorized user' do + let(:owner) { project.owner } + + it 'destroys the pipeline' do + delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) + + expect(response).to have_gitlab_http_status(:no_content) + expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns 404 when it does not exist' do + delete api("/projects/#{project.id}/pipelines/#{non_existing_record_id}", owner) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq '404 Not found' + end + + it 'does not log an audit event' do + expect { delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) }.not_to change { SecurityEvent.count } + end + + context 'when the pipeline has jobs' do + let_it_be(:build) { create(:ci_build, project: project, pipeline: pipeline) } + + it 'destroys associated jobs' do + delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) + + expect(response).to have_gitlab_http_status(:no_content) + expect { build.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + + context 'unauthorized user' do + context 'when user is not member' do + it 'returns a 404' do + delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq '404 Project Not Found' + end + end + + context 'when user is developer' do + let(:developer) { create(:user) } + + before do + project.add_developer(developer) + end + + it 'returns a 403' do + delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", developer) + + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq '403 Forbidden' + end + end + end + end + + describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do + context 'authorized user' do + let_it_be(:pipeline) do + create(:ci_pipeline, project: project, sha: project.commit.id, + ref: project.default_branch) + end + + let_it_be(:build) { create(:ci_build, :failed, pipeline: pipeline) } + + it 'retries failed builds' do + expect do + post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user) + end.to change { pipeline.builds.count }.from(1).to(2) + + expect(response).to have_gitlab_http_status(:created) + expect(build.reload.retried?).to be true + end + end + + context 'unauthorized user' do + it 'does not return a project pipeline' do + post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response['id']).to be nil + end + end + end + + describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do + let_it_be(:pipeline) do + create(:ci_empty_pipeline, project: project, sha: project.commit.id, + ref: project.default_branch) + end + + let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) } + + context 'authorized user' do + it 'retries failed builds', :sidekiq_might_not_need_inline do + post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['status']).to eq('canceled') + end + end + + context 'user without proper access rights' do + let_it_be(:reporter) { create(:user) } + + before do + project.add_reporter(reporter) + end + + it 'rejects the action' do + post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter) + + expect(response).to have_gitlab_http_status(:forbidden) + expect(pipeline.reload.status).to eq('pending') + end + end + end + + describe 'GET /projects/:id/pipelines/:pipeline_id/test_report' do + context 'authorized user' do + subject { get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report", user) } + + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when feature is enabled' do + before do + stub_feature_flags(junit_pipeline_view: true) + end + + context 'when pipeline does not have a test report' do + it 'returns an empty test report' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['total_count']).to eq(0) + end + end + + context 'when pipeline has a test report' do + let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) } + + it 'returns the test report' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['total_count']).to eq(4) + end + end + + context 'when pipeline has corrupt test reports' do + before do + job = create(:ci_build, pipeline: pipeline) + create(:ci_job_artifact, :junit_with_corrupted_data, job: job, project: project) + end + + it 'returns a suite_error' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['test_suites'].first['suite_error']).to eq('JUnit XML parsing failed: 1:1: FATAL: Document is empty') + end + end + end + + context 'when feature is disabled' do + before do + stub_feature_flags(junit_pipeline_view: false) + end + + it 'renders empty response' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'unauthorized user' do + it 'does not return project pipelines' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report", non_member) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq '404 Project Not Found' + end + end + end +end diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb deleted file mode 100644 index 86f3ff54b83..00000000000 --- a/spec/requests/api/pipeline_schedules_spec.rb +++ /dev/null @@ -1,522 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe API::PipelineSchedules do - let_it_be(:developer) { create(:user) } - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, :repository, public_builds: false) } - - before do - project.add_developer(developer) - end - - describe 'GET /projects/:id/pipeline_schedules' do - context 'authenticated user with valid permissions' do - let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) } - - before do - pipeline_schedule.pipelines << build(:ci_pipeline, project: project) - end - - def create_pipeline_schedules(count) - create_list(:ci_pipeline_schedule, count, project: project) - .each do |pipeline_schedule| - create(:user).tap do |user| - project.add_developer(user) - pipeline_schedule.update(owner: user) - end - pipeline_schedule.pipelines << build(:ci_pipeline, project: project) - end - end - - it 'returns list of pipeline_schedules' do - get api("/projects/#{project.id}/pipeline_schedules", developer) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(response).to match_response_schema('pipeline_schedules') - end - - it 'avoids N + 1 queries' do - # We need at least two users to trigger a preload for that relation. - create_pipeline_schedules(1) - - control_count = ActiveRecord::QueryRecorder.new do - get api("/projects/#{project.id}/pipeline_schedules", developer) - end.count - - create_pipeline_schedules(5) - - expect do - get api("/projects/#{project.id}/pipeline_schedules", developer) - end.not_to exceed_query_limit(control_count) - end - - %w[active inactive].each do |target| - context "when scope is #{target}" do - before do - create(:ci_pipeline_schedule, project: project, active: active?(target)) - end - - it 'returns matched pipeline schedules' do - get api("/projects/#{project.id}/pipeline_schedules", developer), params: { scope: target } - - expect(json_response.map { |r| r['active'] }).to all(eq(active?(target))) - end - end - - def active?(str) - str == 'active' - end - end - end - - context 'authenticated user with invalid permissions' do - it 'does not return pipeline_schedules list' do - get api("/projects/#{project.id}/pipeline_schedules", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'unauthenticated user' do - it 'does not return pipeline_schedules list' do - get api("/projects/#{project.id}/pipeline_schedules") - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - end - - describe 'GET /projects/:id/pipeline_schedules/:pipeline_schedule_id' do - let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) } - - before do - pipeline_schedule.variables << build(:ci_pipeline_schedule_variable) - pipeline_schedule.pipelines << build(:ci_pipeline, project: project) - end - - context 'authenticated user with valid permissions' do - it 'returns pipeline_schedule details' do - get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('pipeline_schedule') - end - - it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do - get api("/projects/#{project.id}/pipeline_schedules/-5", developer) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'authenticated user with invalid permissions' do - it 'does not return pipeline_schedules list' do - get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'authenticated user with insufficient permissions' do - before do - project.add_guest(user) - end - - it 'does not return pipeline_schedules list' do - get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'unauthenticated user' do - it 'does not return pipeline_schedules list' do - get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - end - - describe 'POST /projects/:id/pipeline_schedules' do - let(:params) { attributes_for(:ci_pipeline_schedule) } - - context 'authenticated user with valid permissions' do - context 'with required parameters' do - it 'creates pipeline_schedule' do - expect do - post api("/projects/#{project.id}/pipeline_schedules", developer), - params: params - end.to change { project.pipeline_schedules.count }.by(1) - - expect(response).to have_gitlab_http_status(:created) - expect(response).to match_response_schema('pipeline_schedule') - expect(json_response['description']).to eq(params[:description]) - expect(json_response['ref']).to eq(params[:ref]) - expect(json_response['cron']).to eq(params[:cron]) - expect(json_response['cron_timezone']).to eq(params[:cron_timezone]) - expect(json_response['owner']['id']).to eq(developer.id) - end - end - - context 'without required parameters' do - it 'does not create pipeline_schedule' do - post api("/projects/#{project.id}/pipeline_schedules", developer) - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'when cron has validation error' do - it 'does not create pipeline_schedule' do - post api("/projects/#{project.id}/pipeline_schedules", developer), - params: params.merge('cron' => 'invalid-cron') - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to have_key('cron') - end - end - end - - context 'authenticated user with invalid permissions' do - it 'does not create pipeline_schedule' do - post api("/projects/#{project.id}/pipeline_schedules", user), params: params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'unauthenticated user' do - it 'does not create pipeline_schedule' do - post api("/projects/#{project.id}/pipeline_schedules"), params: params - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - end - - describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' do - let(:pipeline_schedule) do - create(:ci_pipeline_schedule, project: project, owner: developer) - end - - context 'authenticated user with valid permissions' do - it 'updates cron' do - put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer), - params: { cron: '1 2 3 4 *' } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('pipeline_schedule') - expect(json_response['cron']).to eq('1 2 3 4 *') - end - - context 'when cron has validation error' do - it 'does not update pipeline_schedule' do - put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer), - params: { cron: 'invalid-cron' } - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to have_key('cron') - end - end - end - - context 'authenticated user with invalid permissions' do - it 'does not update pipeline_schedule' do - put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'unauthenticated user' do - it 'does not update pipeline_schedule' do - put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - end - - describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do - let(:pipeline_schedule) do - create(:ci_pipeline_schedule, project: project, owner: developer) - end - - context 'authenticated user with valid permissions' do - it 'updates owner' do - post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", developer) - - expect(response).to have_gitlab_http_status(:created) - expect(response).to match_response_schema('pipeline_schedule') - end - end - - context 'authenticated user with invalid permissions' do - it 'does not update owner' do - post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'unauthenticated user' do - it 'does not update owner' do - post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership") - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - end - - describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id' do - let(:maintainer) { create(:user) } - - let!(:pipeline_schedule) do - create(:ci_pipeline_schedule, project: project, owner: developer) - end - - before do - project.add_maintainer(maintainer) - end - - context 'authenticated user with valid permissions' do - it 'deletes pipeline_schedule' do - expect do - delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", maintainer) - end.to change { project.pipeline_schedules.count }.by(-1) - - expect(response).to have_gitlab_http_status(:no_content) - end - - it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do - delete api("/projects/#{project.id}/pipeline_schedules/-5", maintainer) - - expect(response).to have_gitlab_http_status(:not_found) - end - - it_behaves_like '412 response' do - let(:request) { api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", maintainer) } - end - end - - context 'authenticated user with invalid permissions' do - let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: maintainer) } - - it 'does not delete pipeline_schedule' do - delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'unauthenticated user' do - it 'does not delete pipeline_schedule' do - delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - end - - describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/play' do - let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } - - let(:route) { ->(id) { "/projects/#{project.id}/pipeline_schedules/#{id}/play" } } - - context 'authenticated user with `:play_pipeline_schedule` permission' do - it 'schedules a pipeline worker' do - project.add_developer(developer) - - expect(RunPipelineScheduleWorker) - .to receive(:perform_async) - .with(pipeline_schedule.id, developer.id) - .and_call_original - post api(route[pipeline_schedule.id], developer) - - expect(response).to have_gitlab_http_status(:created) - end - - it 'renders an error if scheduling failed' do - project.add_developer(developer) - - expect(RunPipelineScheduleWorker) - .to receive(:perform_async) - .with(pipeline_schedule.id, developer.id) - .and_return(nil) - post api(route[pipeline_schedule.id], developer) - - expect(response).to have_gitlab_http_status(:internal_server_error) - end - end - - context 'authenticated user with insufficient access' do - it 'responds with not found' do - project.add_guest(user) - - post api(route[pipeline_schedule.id], user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'unauthenticated user' do - it 'responds with unauthorized' do - post api(route[pipeline_schedule.id]) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - end - - describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables' do - let(:params) { attributes_for(:ci_pipeline_schedule_variable) } - - let_it_be(:pipeline_schedule) do - create(:ci_pipeline_schedule, project: project, owner: developer) - end - - context 'authenticated user with valid permissions' do - context 'with required parameters' do - it 'creates pipeline_schedule_variable' do - expect do - post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer), - params: params.merge(variable_type: 'file') - end.to change { pipeline_schedule.variables.count }.by(1) - - expect(response).to have_gitlab_http_status(:created) - expect(response).to match_response_schema('pipeline_schedule_variable') - expect(json_response['key']).to eq(params[:key]) - expect(json_response['value']).to eq(params[:value]) - expect(json_response['variable_type']).to eq('file') - end - end - - context 'without required parameters' do - it 'does not create pipeline_schedule_variable' do - post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer) - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'when key has validation error' do - it 'does not create pipeline_schedule_variable' do - post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer), - params: params.merge('key' => '!?!?') - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to have_key('key') - end - end - end - - context 'authenticated user with invalid permissions' do - it 'does not create pipeline_schedule_variable' do - post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", user), params: params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'unauthenticated user' do - it 'does not create pipeline_schedule_variable' do - post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables"), params: params - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - end - - describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do - let_it_be(:pipeline_schedule) do - create(:ci_pipeline_schedule, project: project, owner: developer) - end - - let(:pipeline_schedule_variable) do - create(:ci_pipeline_schedule_variable, pipeline_schedule: pipeline_schedule) - end - - context 'authenticated user with valid permissions' do - it 'updates pipeline_schedule_variable' do - put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer), - params: { value: 'updated_value', variable_type: 'file' } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('pipeline_schedule_variable') - expect(json_response['value']).to eq('updated_value') - expect(json_response['variable_type']).to eq('file') - end - end - - context 'authenticated user with invalid permissions' do - it 'does not update pipeline_schedule_variable' do - put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'unauthenticated user' do - it 'does not update pipeline_schedule_variable' do - put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}") - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - end - - describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do - let(:maintainer) { create(:user) } - - let_it_be(:pipeline_schedule) do - create(:ci_pipeline_schedule, project: project, owner: developer) - end - - let!(:pipeline_schedule_variable) do - create(:ci_pipeline_schedule_variable, pipeline_schedule: pipeline_schedule) - end - - before do - project.add_maintainer(maintainer) - end - - context 'authenticated user with valid permissions' do - it 'deletes pipeline_schedule_variable' do - expect do - delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", maintainer) - end.to change { Ci::PipelineScheduleVariable.count }.by(-1) - - expect(response).to have_gitlab_http_status(:accepted) - expect(response).to match_response_schema('pipeline_schedule_variable') - end - - it 'responds with 404 Not Found if requesting non-existing pipeline_schedule_variable' do - delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/____", maintainer) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'authenticated user with invalid permissions' do - let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: maintainer) } - - it 'does not delete pipeline_schedule_variable' do - delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'unauthenticated user' do - it 'does not delete pipeline_schedule_variable' do - delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}") - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - end -end diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb deleted file mode 100644 index b9bc8eabf2c..00000000000 --- a/spec/requests/api/pipelines_spec.rb +++ /dev/null @@ -1,786 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe API::Pipelines do - let_it_be(:user) { create(:user) } - let_it_be(:non_member) { create(:user) } - - # We need to reload as the shared example 'pipelines visibility table' is changing project - let_it_be(:project, reload: true) do - create(:project, :repository, creator: user) - end - - let_it_be(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch, user: user) - end - - before do - project.add_maintainer(user) - end - - describe 'GET /projects/:id/pipelines ' do - it_behaves_like 'pipelines visibility table' - - context 'authorized user' do - it 'returns project pipelines' do - get api("/projects/#{project.id}/pipelines", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['sha']).to match(/\A\h{40}\z/) - expect(json_response.first['id']).to eq pipeline.id - expect(json_response.first['web_url']).to be_present - expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status web_url created_at updated_at]) - end - - context 'when parameter is passed' do - %w[running pending].each do |target| - context "when scope is #{target}" do - before do - create(:ci_pipeline, project: project, status: target) - end - - it 'returns matched pipelines' do - get api("/projects/#{project.id}/pipelines", user), params: { scope: target } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).not_to be_empty - json_response.each { |r| expect(r['status']).to eq(target) } - end - end - end - - context 'when scope is finished' do - before do - create(:ci_pipeline, project: project, status: 'success') - create(:ci_pipeline, project: project, status: 'failed') - create(:ci_pipeline, project: project, status: 'canceled') - end - - it 'returns matched pipelines' do - get api("/projects/#{project.id}/pipelines", user), params: { scope: 'finished' } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).not_to be_empty - json_response.each { |r| expect(r['status']).to be_in(%w[success failed canceled]) } - end - end - - context 'when scope is branches or tags' do - let_it_be(:pipeline_branch) { create(:ci_pipeline, project: project) } - let_it_be(:pipeline_tag) { create(:ci_pipeline, project: project, ref: 'v1.0.0', tag: true) } - - context 'when scope is branches' do - it 'returns matched pipelines' do - get api("/projects/#{project.id}/pipelines", user), params: { scope: 'branches' } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).not_to be_empty - expect(json_response.last['id']).to eq(pipeline_branch.id) - end - end - - context 'when scope is tags' do - it 'returns matched pipelines' do - get api("/projects/#{project.id}/pipelines", user), params: { scope: 'tags' } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).not_to be_empty - expect(json_response.last['id']).to eq(pipeline_tag.id) - end - end - end - - context 'when scope is invalid' do - it 'returns bad_request' do - get api("/projects/#{project.id}/pipelines", user), params: { scope: 'invalid-scope' } - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - Ci::HasStatus::AVAILABLE_STATUSES.each do |target| - context "when status is #{target}" do - before do - create(:ci_pipeline, project: project, status: target) - exception_status = Ci::HasStatus::AVAILABLE_STATUSES - [target] - create(:ci_pipeline, project: project, status: exception_status.sample) - end - - it 'returns matched pipelines' do - get api("/projects/#{project.id}/pipelines", user), params: { status: target } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).not_to be_empty - json_response.each { |r| expect(r['status']).to eq(target) } - end - end - end - - context 'when status is invalid' do - it 'returns bad_request' do - get api("/projects/#{project.id}/pipelines", user), params: { status: 'invalid-status' } - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'when ref is specified' do - before do - create(:ci_pipeline, project: project) - end - - context 'when ref exists' do - it 'returns matched pipelines' do - get api("/projects/#{project.id}/pipelines", user), params: { ref: 'master' } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).not_to be_empty - json_response.each { |r| expect(r['ref']).to eq('master') } - end - end - - context 'when ref does not exist' do - it 'returns empty' do - get api("/projects/#{project.id}/pipelines", user), params: { ref: 'invalid-ref' } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_empty - end - end - end - - context 'when name is specified' do - let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } - - context 'when name exists' do - it 'returns matched pipelines' do - get api("/projects/#{project.id}/pipelines", user), params: { name: user.name } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response.first['id']).to eq(pipeline.id) - end - end - - context 'when name does not exist' do - it 'returns empty' do - get api("/projects/#{project.id}/pipelines", user), params: { name: 'invalid-name' } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_empty - end - end - end - - context 'when username is specified' do - let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } - - context 'when username exists' do - it 'returns matched pipelines' do - get api("/projects/#{project.id}/pipelines", user), params: { username: user.username } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response.first['id']).to eq(pipeline.id) - end - end - - context 'when username does not exist' do - it 'returns empty' do - get api("/projects/#{project.id}/pipelines", user), params: { username: 'invalid-username' } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_empty - end - end - end - - context 'when yaml_errors is specified' do - let_it_be(:pipeline1) { create(:ci_pipeline, project: project, yaml_errors: 'Syntax error') } - let_it_be(:pipeline2) { create(:ci_pipeline, project: project) } - - context 'when yaml_errors is true' do - it 'returns matched pipelines' do - get api("/projects/#{project.id}/pipelines", user), params: { yaml_errors: true } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response.first['id']).to eq(pipeline1.id) - end - end - - context 'when yaml_errors is false' do - it 'returns matched pipelines' do - get api("/projects/#{project.id}/pipelines", user), params: { yaml_errors: false } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response.first['id']).to eq(pipeline2.id) - end - end - - context 'when yaml_errors is invalid' do - it 'returns bad_request' do - get api("/projects/#{project.id}/pipelines", user), params: { yaml_errors: 'invalid-yaml_errors' } - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - end - - context 'when updated_at filters are specified' do - let_it_be(:pipeline1) { create(:ci_pipeline, project: project, updated_at: 2.days.ago) } - let_it_be(:pipeline2) { create(:ci_pipeline, project: project, updated_at: 4.days.ago) } - let_it_be(:pipeline3) { create(:ci_pipeline, project: project, updated_at: 1.hour.ago) } - - it 'returns pipelines with last update date in specified datetime range' do - get api("/projects/#{project.id}/pipelines", user), params: { updated_before: 1.day.ago, updated_after: 3.days.ago } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response.first['id']).to eq(pipeline1.id) - end - end - - context 'when order_by and sort are specified' do - context 'when order_by user_id' do - before do - create_list(:user, 3).each do |some_user| - create(:ci_pipeline, project: project, user: some_user) - end - end - - context 'when sort parameter is valid' do - it 'sorts as user_id: :desc' do - get api("/projects/#{project.id}/pipelines", user), params: { order_by: 'user_id', sort: 'desc' } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).not_to be_empty - - pipeline_ids = Ci::Pipeline.all.order(user_id: :desc).pluck(:id) - expect(json_response.map { |r| r['id'] }).to eq(pipeline_ids) - end - end - - context 'when sort parameter is invalid' do - it 'returns bad_request' do - get api("/projects/#{project.id}/pipelines", user), params: { order_by: 'user_id', sort: 'invalid_sort' } - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - end - - context 'when order_by is invalid' do - it 'returns bad_request' do - get api("/projects/#{project.id}/pipelines", user), params: { order_by: 'lock_version', sort: 'asc' } - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - end - end - end - - context 'unauthorized user' do - it 'does not return project pipelines' do - get api("/projects/#{project.id}/pipelines", non_member) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response).not_to be_an Array - end - end - end - - describe 'POST /projects/:id/pipeline ' do - def expect_variables(variables, expected_variables) - variables.each_with_index do |variable, index| - expected_variable = expected_variables[index] - - expect(variable.key).to eq(expected_variable['key']) - expect(variable.value).to eq(expected_variable['value']) - expect(variable.variable_type).to eq(expected_variable['variable_type']) - end - end - - context 'authorized user' do - context 'with gitlab-ci.yml' do - before do - stub_ci_pipeline_to_return_yaml_file - end - - it 'creates and returns a new pipeline' do - expect do - post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch } - end.to change { project.ci_pipelines.count }.by(1) - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to be_a Hash - expect(json_response['sha']).to eq project.commit.id - end - - context 'variables given' do - let(:variables) { [{ 'variable_type' => 'file', 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] } - - it 'creates and returns a new pipeline using the given variables' do - expect do - post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch, variables: variables } - end.to change { project.ci_pipelines.count }.by(1) - expect_variables(project.ci_pipelines.last.variables, variables) - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to be_a Hash - expect(json_response['sha']).to eq project.commit.id - expect(json_response).not_to have_key('variables') - end - end - - describe 'using variables conditions' do - let(:variables) { [{ 'variable_type' => 'env_var', 'key' => 'STAGING', 'value' => 'true' }] } - - before do - config = YAML.dump(test: { script: 'test', only: { variables: ['$STAGING'] } }) - stub_ci_pipeline_yaml_file(config) - end - - it 'creates and returns a new pipeline using the given variables' do - expect do - post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch, variables: variables } - end.to change { project.ci_pipelines.count }.by(1) - expect_variables(project.ci_pipelines.last.variables, variables) - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to be_a Hash - expect(json_response['sha']).to eq project.commit.id - expect(json_response).not_to have_key('variables') - end - - context 'condition unmatch' do - let(:variables) { [{ 'key' => 'STAGING', 'value' => 'false' }] } - - it "doesn't create a job" do - expect do - post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch } - end.not_to change { project.ci_pipelines.count } - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - end - - it 'fails when using an invalid ref' do - post api("/projects/#{project.id}/pipeline", user), params: { ref: 'invalid_ref' } - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['base'].first).to eq 'Reference not found' - expect(json_response).not_to be_an Array - end - end - - context 'without gitlab-ci.yml' do - context 'without auto devops enabled' do - before do - project.update!(auto_devops_attributes: { enabled: false }) - end - - it 'fails to create pipeline' do - post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch } - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['base'].first).to eq 'Missing CI config file' - expect(json_response).not_to be_an Array - end - end - end - end - - context 'unauthorized user' do - it 'does not create pipeline' do - post api("/projects/#{project.id}/pipeline", non_member), params: { ref: project.default_branch } - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response).not_to be_an Array - end - end - end - - describe 'GET /projects/:id/pipelines/:pipeline_id' do - it_behaves_like 'pipelines visibility table' do - let(:pipelines_api_path) do - "/projects/#{project.id}/pipelines/#{pipeline.id}" - end - - let(:api_response) { response_status == 200 ? response : json_response } - let(:response_200) { match_response_schema('public_api/v4/pipeline/detail') } - end - - context 'authorized user' do - it 'exposes known attributes' do - get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/pipeline/detail') - end - - it 'returns project pipelines' do - get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['sha']).to match(/\A\h{40}\z/) - end - - it 'returns 404 when it does not exist' do - get api("/projects/#{project.id}/pipelines/#{non_existing_record_id}", user) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq '404 Not found' - expect(json_response['id']).to be nil - end - - context 'with coverage' do - before do - create(:ci_build, coverage: 30, pipeline: pipeline) - end - - it 'exposes the coverage' do - get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) - - expect(json_response["coverage"].to_i).to eq(30) - end - end - end - - context 'unauthorized user' do - it 'does not return a project pipeline' do - get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response['id']).to be nil - end - end - end - - describe 'GET /projects/:id/pipelines/latest' do - context 'authorized user' do - let(:second_branch) { project.repository.branches[2] } - - let!(:second_pipeline) do - create(:ci_empty_pipeline, project: project, sha: second_branch.target, - ref: second_branch.name, user: user) - end - - before do - create(:ci_empty_pipeline, project: project, sha: project.commit.parent.id, - ref: project.default_branch, user: user) - end - - context 'default repository branch' do - it 'gets the latest pipleine' do - get api("/projects/#{project.id}/pipelines/latest", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/pipeline/detail') - expect(json_response['ref']).to eq(project.default_branch) - expect(json_response['sha']).to eq(project.commit.id) - end - end - - context 'ref parameter' do - it 'gets the latest pipleine' do - get api("/projects/#{project.id}/pipelines/latest", user), params: { ref: second_branch.name } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/pipeline/detail') - expect(json_response['ref']).to eq(second_branch.name) - expect(json_response['sha']).to eq(second_branch.target) - end - end - end - - context 'unauthorized user' do - it 'does not return a project pipeline' do - get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response['id']).to be nil - end - end - end - - describe 'GET /projects/:id/pipelines/:pipeline_id/variables' do - subject { get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", api_user) } - - let(:api_user) { user } - - context 'user is a mantainer' do - it 'returns pipeline variables empty' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_empty - end - - context 'with variables' do - let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') } - - it 'returns pipeline variables' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to contain_exactly({ "variable_type" => "env_var", "key" => "foo", "value" => "bar" }) - end - end - end - - context 'user is a developer' do - let(:pipeline_owner_user) { create(:user) } - let(:pipeline) { create(:ci_empty_pipeline, project: project, user: pipeline_owner_user) } - - before do - project.add_developer(api_user) - end - - context 'pipeline created by the developer user' do - let(:api_user) { pipeline_owner_user } - let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') } - - it 'returns pipeline variables' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to contain_exactly({ "variable_type" => "env_var", "key" => "foo", "value" => "bar" }) - end - end - - context 'pipeline created is not created by the developer user' do - let(:api_user) { create(:user) } - - it 'does not return pipeline variables' do - subject - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - context 'user is not a project member' do - it 'does not return pipeline variables' do - get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", non_member) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq '404 Project Not Found' - end - end - end - - describe 'DELETE /projects/:id/pipelines/:pipeline_id' do - context 'authorized user' do - let(:owner) { project.owner } - - it 'destroys the pipeline' do - delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) - - expect(response).to have_gitlab_http_status(:no_content) - expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'returns 404 when it does not exist' do - delete api("/projects/#{project.id}/pipelines/#{non_existing_record_id}", owner) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq '404 Not found' - end - - it 'does not log an audit event' do - expect { delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) }.not_to change { SecurityEvent.count } - end - - context 'when the pipeline has jobs' do - let_it_be(:build) { create(:ci_build, project: project, pipeline: pipeline) } - - it 'destroys associated jobs' do - delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) - - expect(response).to have_gitlab_http_status(:no_content) - expect { build.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end - - context 'unauthorized user' do - context 'when user is not member' do - it 'returns a 404' do - delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq '404 Project Not Found' - end - end - - context 'when user is developer' do - let(:developer) { create(:user) } - - before do - project.add_developer(developer) - end - - it 'returns a 403' do - delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", developer) - - expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['message']).to eq '403 Forbidden' - end - end - end - end - - describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do - context 'authorized user' do - let_it_be(:pipeline) do - create(:ci_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) - end - - let_it_be(:build) { create(:ci_build, :failed, pipeline: pipeline) } - - it 'retries failed builds' do - expect do - post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user) - end.to change { pipeline.builds.count }.from(1).to(2) - - expect(response).to have_gitlab_http_status(:created) - expect(build.reload.retried?).to be true - end - end - - context 'unauthorized user' do - it 'does not return a project pipeline' do - post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response['id']).to be nil - end - end - end - - describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do - let_it_be(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) - end - - let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) } - - context 'authorized user' do - it 'retries failed builds', :sidekiq_might_not_need_inline do - post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['status']).to eq('canceled') - end - end - - context 'user without proper access rights' do - let_it_be(:reporter) { create(:user) } - - before do - project.add_reporter(reporter) - end - - it 'rejects the action' do - post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter) - - expect(response).to have_gitlab_http_status(:forbidden) - expect(pipeline.reload.status).to eq('pending') - end - end - end - - describe 'GET /projects/:id/pipelines/:pipeline_id/test_report' do - context 'authorized user' do - subject { get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report", user) } - - let(:pipeline) { create(:ci_pipeline, project: project) } - - context 'when feature is enabled' do - before do - stub_feature_flags(junit_pipeline_view: true) - end - - context 'when pipeline does not have a test report' do - it 'returns an empty test report' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['total_count']).to eq(0) - end - end - - context 'when pipeline has a test report' do - let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) } - - it 'returns the test report' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['total_count']).to eq(4) - end - end - - context 'when pipeline has corrupt test reports' do - before do - job = create(:ci_build, pipeline: pipeline) - create(:ci_job_artifact, :junit_with_corrupted_data, job: job, project: project) - end - - it 'returns a suite_error' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['test_suites'].first['suite_error']).to eq('JUnit XML parsing failed: 1:1: FATAL: Document is empty') - end - end - end - - context 'when feature is disabled' do - before do - stub_feature_flags(junit_pipeline_view: false) - end - - it 'renders empty response' do - subject - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - context 'unauthorized user' do - it 'does not return project pipelines' do - get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report", non_member) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq '404 Project Not Found' - end - end - end -end -- cgit v1.2.1