Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-19 00:10:29 +00:00
parent 27f5b61b11
commit 7e1b27cedb
138 changed files with 1523 additions and 187 deletions

View File

@ -57,12 +57,7 @@ update-qa-cache:
- install_gitlab_gem
script:
- ./scripts/trigger-build omnibus
package-and-qa:
extends:
- .package-and-qa-base
- .qa:rules:package-and-qa
# This job often times out, so temporarily use private runners and a long timeout: https://gitlab.com/gitlab-org/gitlab/-/issues/238563
# These jobs often time out, so temporarily use private runners and a long timeout: https://gitlab.com/gitlab-org/gitlab/-/issues/238563
tags:
- prm
timeout: 4h
@ -71,3 +66,34 @@ package-and-qa:
artifacts: false
- job: build-assets-image
artifacts: false
.package-and-qa-ff-base:
needs:
- detect-tests
variables:
CHANGED_FILES: tmp/changed_files.txt
script:
- export GITLAB_QA_OPTIONS="--set-feature-flags $(scripts/changed-feature-flags --files $(cat $CHANGED_FILES | tr ' ' ',') --state $QA_FF_STATE)"
- echo $GITLAB_QA_OPTIONS
- ./scripts/trigger-build omnibus
package-and-qa:
extends:
- .package-and-qa-base
- .qa:rules:package-and-qa
package-and-qa-ff-enabled:
extends:
- .package-and-qa-base
- .package-and-qa-ff-base
- .qa:rules:package-and-qa:feature-flags
variables:
QA_FF_STATE: "enable"
package-and-qa-ff-disabled:
extends:
- .package-and-qa-base
- .package-and-qa-ff-base
- .qa:rules:package-and-qa:feature-flags
variables:
QA_FF_STATE: "disable"

View File

@ -381,6 +381,9 @@
- "config/helpers/**/*.js"
- "vendor/assets/javascripts/**/*"
.feature-flag-config-patterns: &feature-flag-config-patterns
- "{,ee/}config/feature_flags/**/*.yml"
################
# Shared rules #
################
@ -682,6 +685,9 @@
rules:
- <<: *if-not-ee
when: never
- <<: *if-dot-com-gitlab-org-and-security-merge-request
changes: *feature-flag-config-patterns
when: never
- <<: *if-dot-com-gitlab-org-and-security-merge-request
changes: *ci-qa-patterns
allow_failure: true
@ -695,6 +701,14 @@
- <<: *if-dot-com-gitlab-org-schedule
allow_failure: true
.qa:rules:package-and-qa:feature-flags:
rules:
- <<: *if-not-ee
when: never
- <<: *if-dot-com-gitlab-org-and-security-merge-request
changes: *feature-flag-config-patterns
allow_failure: true
###############
# Rails rules #
###############

View File

@ -69,7 +69,8 @@ export default {
if (isScopedLabel(candidateLabel)) {
const scopedKeyWithDelimiter = `${scopedLabelKey(candidateLabel)}${SCOPED_LABEL_DELIMITER}`;
const currentActiveScopedLabel = state.labels.find(
({ title }) => title.startsWith(scopedKeyWithDelimiter) && title !== candidateLabel.title,
({ set, title }) =>
set && title.startsWith(scopedKeyWithDelimiter) && title !== candidateLabel.title,
);
if (currentActiveScopedLabel) {

View File

@ -9,6 +9,7 @@ module Integrations
:add_pusher,
:alert_events,
:api_key,
:api_token,
:api_url,
:bamboo_url,
:branches_to_be_notified,
@ -74,7 +75,8 @@ module Integrations
:url,
:user_key,
:username,
:webhook
:webhook,
:zentao_product_xid
].freeze
def integration_params

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
module Clusters
class AgentAuthorizationsFinder
def initialize(project)
@project = project
end
def execute
# closest, most-specific authorization for a given agent wins
(project_authorizations + implicit_authorizations + group_authorizations)
.uniq(&:agent_id)
end
private
attr_reader :project
def implicit_authorizations
project.cluster_agents.map do |agent|
Clusters::Agents::ImplicitAuthorization.new(agent: agent)
end
end
# rubocop: disable CodeReuse/ActiveRecord
def project_authorizations
ancestor_ids = project.group ? project.ancestors.select(:id) : project.namespace_id
Clusters::Agents::ProjectAuthorization
.where(project_id: project.id)
.joins(agent: :project)
.preload(agent: :project)
.where(cluster_agents: { projects: { namespace_id: ancestor_ids } })
.to_a
end
def group_authorizations
return [] unless project.group
authorizations = Clusters::Agents::GroupAuthorization.arel_table
ordered_ancestors_cte = Gitlab::SQL::CTE.new(
:ordered_ancestors,
project.group.self_and_ancestors(hierarchy_order: :asc).reselect(:id)
)
cte_join_sources = authorizations.join(ordered_ancestors_cte.table).on(
authorizations[:group_id].eq(ordered_ancestors_cte.table[:id])
).join_sources
Clusters::Agents::GroupAuthorization
.with(ordered_ancestors_cte.to_arel)
.joins(cte_join_sources)
.joins(agent: :project)
.where('projects.namespace_id IN (SELECT id FROM ordered_ancestors)')
.order(Arel.sql('agent_id, array_position(ARRAY(SELECT id FROM ordered_ancestors)::bigint[], agent_group_authorizations.group_id)'))
.select('DISTINCT ON (agent_id) agent_group_authorizations.*')
.preload(agent: :project)
.to_a
end
# rubocop: enable CodeReuse/ActiveRecord
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Clusters
class DeployableAgentsFinder
def initialize(project)
@project = project
end
def execute
project.cluster_agents.ordered_by_name
end
private
attr_reader :project
end
end

View File

@ -132,6 +132,20 @@ module IntegrationsHelper
end
end
def zentao_issue_breadcrumb_link(issue)
link_to issue[:web_url], { target: '_blank', rel: 'noopener noreferrer', class: 'gl-display-flex gl-align-items-center gl-white-space-nowrap' } do
icon = image_tag image_path('logos/zentao.svg'), width: 15, height: 15, class: 'gl-mr-2'
[icon, html_escape(issue[:id])].join.html_safe
end
end
def zentao_issues_show_data
{
issues_show_path: project_integrations_zentao_issue_path(@project, params[:id], format: :json),
issues_list_path: project_integrations_zentao_issues_path(@project)
}
end
extend self
private

View File

@ -14,7 +14,7 @@ class Integration < ApplicationRecord
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao
].freeze
PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[

View File

@ -9,16 +9,25 @@ module Integrations
validates :api_token, presence: true, if: :activated?
validates :zentao_product_xid, presence: true, if: :activated?
def self.feature_flag_enabled?(project)
Feature.enabled?(:zentao_issues_integration, project)
end
# License Level: EEP_FEATURES
def self.issues_license_available?(project)
project&.licensed_feature_available?(:zentao_issues_integration)
end
def data_fields
zentao_tracker_data || self.build_zentao_tracker_data
end
def title
self.class.name.demodulize
'ZenTao'
end
def description
s_("ZentaoIntegration|Use Zentao as this project's issue tracker.")
s_("ZentaoIntegration|Use ZenTao as this project's issue tracker.")
end
def self.to_param
@ -42,28 +51,28 @@ module Integrations
{
type: 'text',
name: 'url',
title: s_('ZentaoIntegration|Zentao Web URL'),
title: s_('ZentaoIntegration|ZenTao Web URL'),
placeholder: 'https://www.zentao.net',
help: s_('ZentaoIntegration|Base URL of the Zentao instance.'),
help: s_('ZentaoIntegration|Base URL of the ZenTao instance.'),
required: true
},
{
type: 'text',
name: 'api_url',
title: s_('ZentaoIntegration|Zentao API URL (optional)'),
title: s_('ZentaoIntegration|ZenTao API URL (optional)'),
help: s_('ZentaoIntegration|If different from Web URL.')
},
{
type: 'password',
name: 'api_token',
title: s_('ZentaoIntegration|Zentao API token'),
title: s_('ZentaoIntegration|ZenTao API token'),
non_empty_password_title: s_('ZentaoIntegration|Enter API token'),
required: true
},
{
type: 'text',
name: 'zentao_product_xid',
title: s_('ZentaoIntegration|Zentao Product ID'),
title: s_('ZentaoIntegration|ZenTao Product ID'),
required: true
}
]

View File

@ -18,7 +18,7 @@ class Namespace < ApplicationRecord
ignore_column :delayed_project_removal, remove_with: '14.1', remove_after: '2021-05-22'
# Tells ActiveRecord not to store the full class name, in order to space some space
# Tells ActiveRecord not to store the full class name, in order to save some space
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69794
self.store_full_sti_class = false
self.store_full_class_name = false

View File

@ -1453,7 +1453,10 @@ class Project < ApplicationRecord
end
def disabled_integrations
[:zentao]
disabled_integrations = []
disabled_integrations << :zentao unless ::Integrations::Zentao.feature_flag_enabled?(self)
disabled_integrations
end
def find_or_initialize_integration(name)

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340166
milestone: '14.3'
type: development
group: group::configure
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: zentao_issues_integration
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69602
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338775
milestone: '14.4'
type: development
group: group::integrations
default_enabled: false

View File

@ -0,0 +1,22 @@
---
key_path: counts.projects_zentao_active
name: count_all_projects_zentao_active
description: Count of projects with active Zentao integrations
product_section: dev
product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338178
time_frame: all
data_source: database
data_category: Operational
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,22 @@
---
key_path: counts.groups_zentao_active
name: count_all_groups_zentao_active
description: Count of groups with active Zentao integrations
product_section: dev
product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338178
time_frame: all
data_source: database
data_category: Operational
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,22 @@
---
key_path: counts.instances_zentao_active
name: count_all_instances_zentao_active
description: Count of instances with active Zentao integrations
product_section: dev
product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338178
time_frame: all
data_source: database
data_category: Operational
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,22 @@
---
key_path: counts.projects_inheriting_zentao_active
name: count_all_projects_inheriting_zentao_active
description: Count of projects that inherit active Zentao integrations
product_section: dev
product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338178
time_frame: all
data_source: database
data_category: Operational
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,22 @@
---
key_path: counts.groups_inheriting_zentao_active
name: count_all_groups_inheriting_zentao_active
description: Count of groups that inherit active Zentao integrations
product_section: dev
product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338178
time_frame: all
data_source: database
data_category: Operational
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -851,6 +851,10 @@ of choice already. Some examples include [HAProxy](https://www.haproxy.org/)
Big-IP LTM, and Citrix Net Scaler. This documentation outlines what ports
and protocols you need configure.
WARNING:
Long-running background jobs can maintain an idle connection with Praefect for up 6 hours. Set your load balancer timeout to be at least
6 hours long.
| LB Port | Backend Port | Protocol |
|:--------|:-------------|:---------|
| 2305 | 2305 | TCP |

View File

@ -101,8 +101,12 @@ To look up a project's hash path using a Rails console:
#### From hashed path to project name
Administrators can look up a project's name from its hashed storage path using a Rails console. To
look up a project's name from its hashed storage path:
Administrators can look up a project's name from its hashed storage path using:
- A Rails console.
- The `config` file in the `*.git` directory.
To look up a project's name using the Rails console:
1. Start a [Rails console](operations/rails_console.md#starting-a-rails-console-session).
1. Run a command similar to this example:
@ -121,6 +125,14 @@ The output includes the project ID and the project name. For example:
=> #<Project id:16 it/supportteam/ticketsystem>
```
To look up a project's name using the `config` file in the `*.git` directory:
1. Navigate to the to the `*.git` directory. This directory is located in `/var/opt/gitlab/git-data/repositories/@hashed/`, where the first four
characters of the hash are the first two directories in the path under `@hashed/`. For example, on a default Omnibus GitLab installation the
`*.git` directory of the hash `b17eb17ef6d19c7a5b1ee83b907c595526dcb1eb06db8227d650d5dda0a9f4ce8cd9` would be
`/var/opt/gitlab/git-data/repositories/@hashed/b1/7e/b17ef6d19c7a5b1ee83b907c595526dcb1eb06db8227d650d5dda0a9f4ce8cd9.git`.
1. Open the `config` file and locate the `fullpath=` key under `[gitlab]`.
### Hashed object pools
> [Introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/1606) in GitLab 12.1.

View File

@ -16578,6 +16578,7 @@ State of a Sentry error.
| <a id="servicetypeunify_circuit_service"></a>`UNIFY_CIRCUIT_SERVICE` | UnifyCircuitService type. |
| <a id="servicetypewebex_teams_service"></a>`WEBEX_TEAMS_SERVICE` | WebexTeamsService type. |
| <a id="servicetypeyoutrack_service"></a>`YOUTRACK_SERVICE` | YoutrackService type. |
| <a id="servicetypezentao_service"></a>`ZENTAO_SERVICE` | ZentaoService type. |
### `SharedRunnersSetting`

View File

@ -79,11 +79,21 @@ for details.
End-to-end tests should pass with a feature flag enabled before it is enabled on Staging or on GitLab.com. Tests that need to be updated should be identified as part of [quad-planning](https://about.gitlab.com/handbook/engineering/quality/quad-planning/). The relevant [counterpart Software Engineer in Test](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors) is responsible for updating the tests or assisting another engineer to do so. However, if a change does not go through quad-planning and a required test update is not made, test failures could block deployment.
If a test enables a feature flag as describe above, it is sufficient to run the `package-and-qa` job in a merge request containing the relevant changes.
Or, if the feature flag and relevant changes have already been merged, you can confirm that the tests
pass on `main`. The end-to-end tests run on `main` every two hours, and the results are posted to a [Test
Session Report, which is available in the testcase-sessions project](https://gitlab.com/gitlab-org/quality/testcase-sessions/-/issues?label_name%5B%5D=found%3Amaster).
### Automatic test execution when a feature flag definition changes
If the relevant tests do not enable the feature flag themselves, you can check if the tests will need
to be updated by opening a draft merge request that enables the flag by default and then running the `package-and-qa` job.
If a merge request adds or edits a [feature flag definition file](../../feature_flags/index.md#feature-flag-definition-and-validation),
two `package-and-qa` jobs will be included automatically in the merge request pipeline. One job will enable the defined
feature flag and the other will disable it. The jobs execute the same suite of tests to confirm that they pass with if
the feature flag is either enabled or disabled.
### Test execution during feature development
If an end-to-end test enables a feature flag, the end-to-end test suite can be used to test changes in a merge request
by running the `package-and-qa` job in the merge request pipeline. If the feature flag and relevant changes have already been merged, you can confirm that the tests
pass on the default branch. The end-to-end tests run on the default branch every two hours, and the results are posted to a [Test
Session Report, which is available in the testcase-sessions project](https://gitlab.com/gitlab-org/quality/testcase-sessions/-/issues?label_name%5B%5D=found%3Amain).
If the relevant tests do not enable the feature flag themselves, you can check if the tests will need to be updated by opening
a draft merge request that enables the flag by default via a [feature flag definition file](../../feature_flags/index.md#feature-flag-definition-and-validation).
That will [automatically execute the end-to-end test suite](#automatic-test-execution-when-a-feature-flag-definition-changes).
The merge request can be closed once the tests pass. If you need assistance to update the tests, please contact the relevant [stable counterpart in the Quality department](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors), or any Software Engineer in Test if there is no stable counterpart for your group.

View File

@ -61,14 +61,35 @@ Since the migration files are not autoloaded by Rails, you must manually
load the migration file. To do so, you can use the `require_migration!` helper method
which can automatically load the correct migration file based on the spec filename.
For example, if your spec file is named as `populate_foo_column_spec.rb` then the
helper method tries to load `${schema_version}_populate_foo_column.rb` migration file.
In case there is no pattern between your spec file and the actual migration file,
you can provide the migration filename without the schema version, like so:
In GitLab 14.4 and later, you can use `require_migration!` to load migration files from spec files
that contain the schema version in the filename (for example,
`2021101412150000_populate_foo_column_spec.rb`).
```ruby
require_migration!('populate_foo_column')
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe PopulateFooColumn do
...
end
```
In some cases, you must require multiple migration files to use them in your specs. Here, there's no
pattern between your spec file and the other migration file. You can provide the migration filename
like so:
```ruby
# frozen_string_literal: true
require 'spec_helper'
require_migration!
require_migration!('populate_bar_column')
RSpec.describe PopulateFooColumn do
...
end
```
#### `table`

View File

@ -44,5 +44,3 @@ Clicking an alert's row opens the alert drawer, which shows more information abo
can also create an incident from the alert and update the alert status in the alert drawer.
Clicking an alert's name takes the user to the [alert details page](../../../operations/incident_management/alerts.md#alert-details-page).
For information on work in progress for the alerts dashboard, see [this epic](https://gitlab.com/groups/gitlab-org/-/epics/5041).

View File

@ -153,9 +153,9 @@ gitops:
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
ask an administrator to [enable the `group_authorized_agents` flag](../../../administration/feature_flags.md).
On GitLab.com, this feature is available.
On self-managed GitLab, by default this feature is available. To hide the
feature, ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `group_authorized_agents`. On
GitLab.com, this feature is available.
If you use the same cluster across multiple projects, you can set up the CI/CD Tunnel
to grant the Agent access to one or more groups. This way, all the projects that belong

View File

@ -177,6 +177,36 @@ module API
present current_authenticated_job, with: Entities::Ci::Job
end
desc 'Get current agents' do
detail 'Retrieves a list of agents for the given job token'
end
route_setting :authentication, job_token_allowed: true
get '/allowed_agents', feature_category: :kubernetes_management do
validate_current_authenticated_job
status 200
pipeline = current_authenticated_job.pipeline
project = current_authenticated_job.project
allowed_agents =
if Feature.enabled?(:group_authorized_agents, project, default_enabled: :yaml)
agent_authorizations = Clusters::AgentAuthorizationsFinder.new(project).execute
Entities::Clusters::AgentAuthorization.represent(agent_authorizations)
else
associated_agents = Clusters::DeployableAgentsFinder.new(project).execute
Entities::Clusters::Agent.represent(associated_agents)
end
{
allowed_agents: allowed_agents,
job: Entities::Ci::JobRequest::JobInfo.represent(current_authenticated_job),
pipeline: Entities::Ci::PipelineBasic.represent(pipeline),
project: Entities::ProjectIdentity.represent(project),
user: Entities::UserBasic.represent(current_user)
}
end
end
helpers do
@ -202,5 +232,3 @@ module API
end
end
end
API::Ci::Jobs.prepend_mod_with('API::Ci::Jobs')

View File

@ -254,7 +254,7 @@ module API
type: Boolean,
desc: 'DEPRECATED: This parameter has no effect since SSL verification will always be enabled'
}
],
],
'campfire' => [
{
required: true,
@ -768,7 +768,33 @@ module API
desc: 'The Webex Teams webhook. For example, https://api.ciscospark.com/v1/webhooks/incoming/...'
},
chat_notification_events
].flatten
].flatten,
'zentao' => [
{
required: true,
name: :url,
type: String,
desc: 'The base URL to the ZenTao instance web interface which is being linked to this GitLab project. For example, https://www.zentao.net'
},
{
required: false,
name: :api_url,
type: String,
desc: 'The base URL to the ZenTao instance API. Web URL value will be used if not set. For example, https://www.zentao.net'
},
{
required: true,
name: :api_token,
type: String,
desc: 'The API token created from ZenTao dashboard'
},
{
required: true,
name: :zentao_product_xid,
type: String,
desc: 'The product ID of ZenTao project'
}
]
}
end
@ -805,7 +831,8 @@ module API
::Integrations::Slack,
::Integrations::SlackSlashCommands,
::Integrations::Teamcity,
::Integrations::Youtrack
::Integrations::Youtrack,
::Integrations::Zentao
]
end

View File

@ -10,7 +10,8 @@ class Gitlab::Ci::Build::AutoRetry
RETRY_OVERRIDES = {
ci_quota_exceeded: 0,
no_matching_runner: 0,
missing_dependency_failure: 0
missing_dependency_failure: 0,
forward_deployment_failure: 0
}.freeze
def initialize(build)

View File

@ -15,10 +15,8 @@ module Gitlab
end
def ping
response = fetch_product(zentao_product_xid)
response = fetch_product(zentao_product_xid) rescue {}
active = response.fetch('deleted') == '0' rescue false
if active
{ success: true }
else
@ -31,25 +29,30 @@ module Gitlab
end
def fetch_issues(params = {})
get("products/#{zentao_product_xid}/issues",
params.reverse_merge(page: 1, limit: 20))
get("products/#{zentao_product_xid}/issues", params)
end
def fetch_issue(issue_id)
raise Gitlab::Zentao::Client::Error unless issue_id_pattern.match(issue_id)
get("issues/#{issue_id}")
end
private
def issue_id_pattern
/\A\S+-\d+\z/
end
def get(path, params = {})
options = { headers: headers, query: params }
response = Gitlab::HTTP.get(url(path), options)
return {} unless response.success?
raise Gitlab::Zentao::Client::Error unless response.success?
Gitlab::Json.parse(response.body)
rescue JSON::ParserError
{}
raise Gitlab::Zentao::Client::Error
end
def url(path)

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
module Gitlab
module Zentao
class Query
STATUSES = %w[all opened closed].freeze
ISSUES_DEFAULT_LIMIT = 20
ISSUES_MAX_LIMIT = 50
attr_reader :client, :params
def initialize(integration, params)
@client = Client.new(integration)
@params = params
end
def issues
issues_response = client.fetch_issues(query_options)
return [] if issues_response.blank?
Kaminari.paginate_array(
issues_response['issues'],
limit: issues_response['limit'],
total_count: issues_response['total']
)
end
def issue
issue_response = client.fetch_issue(params[:id])
issue_response['issue']
end
private
def query_options
{
order: query_order,
status: query_status,
labels: query_labels,
page: query_page,
limit: query_limit,
search: query_search
}
end
def query_page
params[:page].presence || 1
end
def query_limit
limit = params[:limit].presence || ISSUES_DEFAULT_LIMIT
[limit.to_i, ISSUES_MAX_LIMIT].min
end
def query_search
params[:search] || ''
end
def query_order
key, order = params['sort'].to_s.split('_', 2)
zentao_key = (key == 'created' ? 'openedDate' : 'lastEditedDate')
zentao_order = (order == 'asc' ? 'asc' : 'desc')
"#{zentao_key}_#{zentao_order}"
end
def query_status
return params[:state] if params[:state].present? && params[:state].in?(STATUSES)
'opened'
end
def query_labels
(params[:labels].presence || []).join(',')
end
end
end
end

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
module Sidebars
module Projects
module Menus
class ZentaoMenu < ::Sidebars::Menu
override :configure_menu_items
def configure_menu_items
render?.tap do |render|
break unless render
add_items
end
end
override :link
def link
zentao_integration.url
end
override :title
def title
s_('ZentaoIntegration|ZenTao issues')
end
override :title_html_options
def title_html_options
{
id: 'js-onboarding-settings-link'
}
end
override :image_path
def image_path
'logos/zentao.svg'
end
# Hardcode sizes so image doesn't flash before CSS loads https://gitlab.com/gitlab-org/gitlab/-/issues/321022
override :image_html_options
def image_html_options
{
size: 16
}
end
override :render?
def render?
return false if zentao_integration.blank?
zentao_integration.active?
end
def add_items
add_item(open_zentao_menu_item)
end
private
def zentao_integration
@zentao_integration ||= context.project.zentao_integration
end
def open_zentao_menu_item
::Sidebars::MenuItem.new(
title: s_('ZentaoIntegration|Open ZenTao'),
link: zentao_integration.url,
active_routes: {},
item_id: :open_zentao,
sprite_icon: 'external-link',
container_html_options: {
target: '_blank',
rel: 'noopener noreferrer'
}
)
end
end
end
end
end
::Sidebars::Projects::Menus::ZentaoMenu.prepend_mod

View File

@ -23,6 +23,7 @@ module Sidebars
add_menu(Sidebars::Projects::Menus::RepositoryMenu.new(context))
add_menu(Sidebars::Projects::Menus::IssuesMenu.new(context))
add_menu(Sidebars::Projects::Menus::ExternalIssueTrackerMenu.new(context))
add_menu(Sidebars::Projects::Menus::ZentaoMenu.new(context)) if ::Integrations::Zentao.feature_flag_enabled?(context.project)
add_menu(Sidebars::Projects::Menus::MergeRequestsMenu.new(context))
add_menu(Sidebars::Projects::Menus::CiCdMenu.new(context))
add_menu(Sidebars::Projects::Menus::SecurityComplianceMenu.new(context))

View File

@ -39678,7 +39678,13 @@ msgstr ""
msgid "ZenTaoIntegration|ZenTao user"
msgstr ""
msgid "ZentaoIntegration|Base URL of the Zentao instance."
msgid "Zentao issues"
msgstr ""
msgid "ZentaoIntegration|An error occurred while requesting data from the ZenTao service."
msgstr ""
msgid "ZentaoIntegration|Base URL of the ZenTao instance."
msgstr ""
msgid "ZentaoIntegration|Enter API token"
@ -39687,19 +39693,31 @@ msgstr ""
msgid "ZentaoIntegration|If different from Web URL."
msgstr ""
msgid "ZentaoIntegration|Use Zentao as this project's issue tracker."
msgid "ZentaoIntegration|Issue list"
msgstr ""
msgid "ZentaoIntegration|Zentao API URL (optional)"
msgid "ZentaoIntegration|Open ZenTao"
msgstr ""
msgid "ZentaoIntegration|Zentao API token"
msgid "ZentaoIntegration|Use ZenTao as this project's issue tracker."
msgstr ""
msgid "ZentaoIntegration|Zentao Product ID"
msgid "ZentaoIntegration|ZenTao API URL (optional)"
msgstr ""
msgid "ZentaoIntegration|Zentao Web URL"
msgid "ZentaoIntegration|ZenTao API token"
msgstr ""
msgid "ZentaoIntegration|ZenTao Product ID"
msgstr ""
msgid "ZentaoIntegration|ZenTao Web URL"
msgstr ""
msgid "ZentaoIntegration|ZenTao issues"
msgstr ""
msgid "ZentaoIntegration|Zentao issues"
msgstr ""
msgid "Zoom meeting added"

View File

@ -77,17 +77,24 @@ module QA
def log_fabrication(method, resource, parents, args)
start = Time.now
yield.tap do
Support::FabricationTracker.start_fabrication
result = yield.tap do
fabrication_time = Time.now - start
Support::FabricationTracker.save_fabrication(:"#{method}_fabrication", fabrication_time * 1000)
Runtime::Logger.debug do
msg = ["==#{'=' * parents.size}>"]
msg << "Built a #{name}"
msg << "as a dependency of #{parents.last}" if parents.any?
msg << "via #{method}"
msg << "in #{Time.now - start} seconds"
msg << "in #{fabrication_time} seconds"
msg.join(' ')
end
end
Support::FabricationTracker.finish_fabrication
result
end
# Define custom attribute

View File

@ -9,6 +9,7 @@ module QA
Project.fabricate! do |resource|
resource.name = 'project-for-issues'
resource.description = 'project for adding issues'
resource.api_client = api_client
end
end
@ -93,6 +94,52 @@ module QA
attempts: attempts
)
end
# Object comparison
#
# @param [QA::Resource::Issue] other
# @return [Boolean]
def ==(other)
other.is_a?(Issue) && comparable_issue == other.comparable_issue
end
# Override inspect for a better rspec failure diff output
#
# @return [String]
def inspect
JSON.pretty_generate(comparable_issue)
end
protected
# Return subset of fields for comparing issues
#
# @return [Hash]
def comparable_issue
reload! if api_response.nil?
api_resource.slice(
:state,
:description,
:type,
:title,
:labels,
:milestone,
:upvotes,
:downvotes,
:merge_requests_count,
:user_notes_count,
:due_date,
:has_tasks,
:task_status,
:confidential,
:discussion_locked,
:issue_type,
:task_completion_status,
:closed_at,
:created_at
)
end
end
end
end

View File

@ -5,15 +5,16 @@ require 'active_support/core_ext/object/blank'
module QA
module Runtime
class Feature
SetFeatureError = Class.new(RuntimeError)
AuthorizationError = Class.new(RuntimeError)
UnknownScopeError = Class.new(RuntimeError)
UnknownStateError = Class.new(RuntimeError)
class << self
# Documentation: https://docs.gitlab.com/ee/api/features.html
include Support::API
SetFeatureError = Class.new(RuntimeError)
AuthorizationError = Class.new(RuntimeError)
UnknownScopeError = Class.new(RuntimeError)
def remove(key)
request = Runtime::API::Request.new(api_client, "/features/#{key}")
response = delete(request.url)
@ -30,6 +31,23 @@ module QA
set_and_verify(key, enable: false, **scopes)
end
# Set one or more flags to their specified state.
#
# @param [Hash] flags The feature flags and desired values, e.g., { 'flag1' => 'enabled', 'flag2' => "disabled" }
# @param [Hash] scopes The scope (user, project, group) to apply the feature flag to.
def set(flags, **scopes)
flags.each_pair do |flag, state|
case state
when 'enabled', 'enable', 'true', 1, true
enable(flag, **scopes)
when 'disabled', 'disable', 'false', 0, false
disable(flag, **scopes)
else
raise UnknownStateError, "Unknown feature flag state: #{state}"
end
end
end
def enabled?(key, **scopes)
feature = JSON.parse(get_features).find { |flag| flag['name'] == key.to_s }
feature && (feature['state'] == 'on' || feature['state'] == 'conditional' && scopes.present? && enabled_scope?(feature['gates'], **scopes))
@ -47,15 +65,15 @@ module QA
scopes.each do |key, value|
case key
when :project, :group, :user
actors = gates.filter { |i| i['key'] == 'actors' }.first['value']
break actors.include?("#{key.to_s.capitalize}:#{value.id}")
actors = gates.find { |i| i['key'] == 'actors' }['value']
return actors.include?("#{key.to_s.capitalize}:#{value.id}")
when :feature_group
groups = gates.filter { |i| i['key'] == 'groups' }.first['value']
break groups.include?(value)
else
raise UnknownScopeError, "Unknown scope: #{key}"
groups = gates.find { |i| i['key'] == 'groups' }['value']
return groups.include?(value)
end
end
raise UnknownScopeError, "Unknown scope in: #{scopes}"
end
def get_features

View File

@ -17,6 +17,22 @@ module QA
arguments = OptionParser.new do |parser|
options.to_a.each do |opt|
# The argument for the --set-feature-flags option should look something like "flag1=enabled,flag2=disabled"
# Here we translate that string into a hash, e.g.: { 'flag1' => 'enabled', 'flag2' => "disabled" }
if opt.name == :set_feature_flags
parser.on(opt.arg, opt.desc) do |flags|
value = flags.split(',').each_with_object({}) do |pair, hash|
flag_name, flag_value = pair.split('=')
raise '--set-feature-flags requires flag name and flag state for each flag, e.g., flag1=enabled,flag2=disabled' unless flag_name && flag_value
hash[flag_name] = flag_value
end
Runtime::Scenario.define(opt.name, value)
end
next
end
parser.on(opt.arg, opt.desc) do |value|
Runtime::Scenario.define(opt.name, value)
end

View File

@ -8,6 +8,9 @@ module QA
attribute :gitlab_address, '--address URL', 'Address of the instance to test'
attribute :enable_feature, '--enable-feature FEATURE_FLAG', 'Enable a feature before running tests'
attribute :disable_feature, '--disable-feature FEATURE_FLAG', 'Disable a feature before running tests'
attribute :set_feature_flags, '--set-feature-flags FEATURE_FLAGS',
'Set one or more feature flags before running tests. ' \
'Specify FEATURE_FLAGS as comma-separated flag=state pairs, e.g., "flag1=enabled,flag2=disabled"'
attribute :parallel, '--parallel', 'Execute tests in parallel'
attribute :loop, '--loop', 'Execute test repeatedly'
end

View File

@ -38,8 +38,8 @@ module QA
Runtime::Release.perform_before_hooks
Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature)
Runtime::Feature.disable(options[:disable_feature]) if options.key?(:disable_feature) && (@feature_enabled = Runtime::Feature.enabled?(options[:disable_feature]))
Runtime::Feature.set(options[:set_feature_flags]) if options.key?(:set_feature_flags)
Specs::Runner.perform do |specs|
specs.tty = true

View File

@ -43,6 +43,12 @@ module QA
sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
end
after do
user.remove_via_api!
ensure
Runtime::Feature.disable(:top_level_group_creation_enabled) if staging?
end
context 'with subgroups and labels' do
let(:subgroup) do
Resource::Group.fabricate_via_api! do |group|
@ -155,12 +161,6 @@ module QA
expect(imported_member.access_level).to eq(Resource::Members::AccessLevel::DEVELOPER)
end
end
after do
user.remove_via_api!
ensure
Runtime::Feature.disable(:top_level_group_creation_enabled) if staging?
end
end
end
end

View File

@ -44,6 +44,10 @@ module QA
end
end
let(:imported_projects) do
imported_group.reload!.projects
end
before do
Runtime::Feature.enable(:bulk_import_projects)
Runtime::Feature.enable(:top_level_group_creation_enabled) if staging?
@ -67,13 +71,48 @@ module QA
) do
expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration)
imported_projects = imported_group.reload!.projects
aggregate_failures do
expect(imported_projects.count).to eq(1)
expect(imported_projects.first).to eq(source_project)
end
end
end
context 'with project issues' do
let(:source_issue) do
Resource::Issue.fabricate_via_api! do |issue|
issue.api_client = api_client
issue.project = source_project
issue.labels = %w[label_one label_two]
end
end
let(:imported_issues) do
imported_projects.first.issues
end
let(:imported_issue) do
issue = imported_issues.first
Resource::Issue.init do |resource|
resource.api_client = api_client
resource.project = imported_projects.first
resource.iid = issue[:iid]
end
end
before do
source_issue # fabricate source group, project, issue
end
it 'successfully imports issue' do
expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration)
aggregate_failures do
expect(imported_issues.count).to eq(1)
expect(imported_issue.reload!).to eq(source_issue)
end
end
end
end
end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
module QA
module Support
# Threadsafe fabrication time tracker
#
# Ongoing fabrication is added to callstack by start_fabrication and taken out by finish_fabrication
#
# Fabrication runtime is saved only for the first fabrication in the stack to properly represent the real time
# fabrications might take as top level fabrication runtime will always include nested fabrications runtime
#
class FabricationTracker
class << self
# Start fabrication and increment ongoing fabrication count
#
# @return [void]
def start_fabrication
Thread.current[:fabrications_ongoing] = 0 unless Thread.current.key?(:fabrications_ongoing)
Thread.current[:fabrications_ongoing] += 1
end
# Finish fabrication and decrement ongoing fabrication count
#
# @return [void]
def finish_fabrication
Thread.current[:fabrications_ongoing] -= 1
end
# Save fabrication time if it's first in fabrication stack
#
# @param [Symbol] type
# @param [Symbol] time
# @return [void]
def save_fabrication(type, time)
return unless Thread.current.key?(type)
return unless top_level_fabrication?
Thread.current[type] += time
end
private
# Check if current fabrication is the only one in the stack
#
# @return [Boolean]
def top_level_fabrication?
Thread.current[:fabrications_ongoing] == 1
end
end
end
end
end

View File

@ -57,6 +57,8 @@ module QA
# @return [Hash]
def test_stats(example)
file_path = example.metadata[:file_path].gsub('./qa/specs/features', '')
api_fabrication = ((example.metadata[:api_fabrication] || 0) * 1000).round
ui_fabrication = ((example.metadata[:browser_ui_fabrication] || 0) * 1000).round
{
name: 'test-stats',
@ -76,6 +78,9 @@ module QA
fields: {
id: example.id,
run_time: (example.execution_result.run_time * 1000).round,
api_fabrication: api_fabrication,
ui_fabrication: ui_fabrication,
total_fabrication: api_fabrication + ui_fabrication,
retry_attempts: example.metadata[:retry_attempts] || 0,
job_url: QA::Runtime::Env.ci_job_url,
pipeline_url: env('CI_PIPELINE_URL'),
@ -98,14 +103,18 @@ module QA
#
# @return [String]
def job_name
@job_name ||= QA::Runtime::Env.ci_job_name.gsub(%r{ \d{1,2}/\d{1,2}}, '')
@job_name ||= QA::Runtime::Env.ci_job_name&.gsub(%r{ \d{1,2}/\d{1,2}}, '')
end
# Single common timestamp for all exported example metrics to keep data points consistently grouped
#
# @return [Time]
def time
@time ||= DateTime.strptime(env('CI_PIPELINE_CREATED_AT')).to_time
@time ||= begin
return Time.now unless env('CI_PIPELINE_CREATED_AT')
DateTime.strptime(env('CI_PIPELINE_CREATED_AT')).to_time
end
end
# Is a merge request execution

View File

@ -175,6 +175,20 @@ RSpec.describe QA::Runtime::Feature do
expect(described_class.enabled?(feature_flag)).to be_truthy
end
it 'raises an error when the scope is unknown' do
expect(QA::Runtime::API::Request)
.to receive(:new)
.with(api_client, "/features")
.and_return(request)
expect(described_class)
.to receive(:get)
.and_return(
Struct.new(:code, :body)
.new(200, %([{ "name": "a_flag", "state": "conditional", "gates": { "key": "groups", "value": ["foo"] } }])))
expect { described_class.enabled?(feature_flag, scope: 'foo') }.to raise_error(QA::Runtime::Feature::UnknownScopeError)
end
context 'when a project scope is provided' do
it_behaves_like 'checks a feature flag' do
let(:scope) { :project }
@ -212,4 +226,38 @@ RSpec.describe QA::Runtime::Feature do
end
end
end
describe '.set' do
let(:scope) { { scope: 'actor' } }
it 'raises an error when the flag state is unknown' do
expect(described_class).not_to receive(:enable)
expect(described_class).not_to receive(:disable)
expect { described_class.set({ foo: 'bar' }, **scope) }.to raise_error(QA::Runtime::Feature::UnknownStateError, 'Unknown feature flag state: bar')
end
it 'enables feature flags' do
expect(described_class).to receive(:enable).with(:flag1, scope)
expect(described_class).to receive(:enable).with(:flag2, scope)
expect(described_class).not_to receive(:disable)
described_class.set({ flag1: 'enabled', flag2: 'enable' }, **scope)
end
it 'disables feature flags' do
expect(described_class).to receive(:disable).with(:flag1, scope)
expect(described_class).to receive(:disable).with(:flag2, scope)
expect(described_class).not_to receive(:enable)
described_class.set({ flag1: 'disable', flag2: 'disable' }, **scope)
end
it 'enables and disables feature flags' do
expect(described_class).to receive(:enable).with(:flag1, scope)
expect(described_class).to receive(:disable).with(:flag2, scope)
described_class.set({ flag1: 'enabled', flag2: 'disabled' }, **scope)
end
end
end

View File

@ -27,8 +27,12 @@ RSpec.configure do |config|
config.add_formatter QA::Support::Formatters::QuarantineFormatter
config.add_formatter QA::Support::Formatters::TestStatsFormatter if QA::Runtime::Env.export_metrics?
config.before do |example|
config.prepend_before do |example|
QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n")
# Reset fabrication counters tracked in resource base
Thread.current[:api_fabrication] = 0
Thread.current[:browser_ui_fabrication] = 0
end
config.after do
@ -36,6 +40,12 @@ RSpec.configure do |config|
QA::Git::Repository.new.delete_netrc
end
# Add fabrication time to spec metadata
config.append_after do |example|
example.metadata[:api_fabrication] = Thread.current[:api_fabrication]
example.metadata[:browser_ui_fabrication] = Thread.current[:browser_ui_fabrication]
end
config.after(:context) do
if !QA::Runtime::Browser.blank_page? && QA::Page::Main::Menu.perform(&:signed_in?)
QA::Page::Main::Menu.perform(&:sign_out)

View File

@ -20,6 +20,8 @@ describe QA::Support::Formatters::TestStatsFormatter do
let(:influx_write_api) { instance_double('InfluxDB2::WriteApi', write: nil) }
let(:stage) { '1_manage' }
let(:file_path) { "./qa/specs/features/#{stage}/subfolder/some_spec.rb" }
let(:ui_fabrication) { 0 }
let(:api_fabrication) { 0 }
let(:influx_client_args) do
{
@ -48,6 +50,9 @@ describe QA::Support::Formatters::TestStatsFormatter do
fields: {
id: './spec/support/formatters/test_stats_formatter_spec.rb[1:1]',
run_time: 0,
api_fabrication: api_fabrication * 1000,
ui_fabrication: ui_fabrication * 1000,
total_fabrication: (api_fabrication + ui_fabrication) * 1000,
retry_attempts: 0,
job_url: ci_job_url,
pipeline_url: ci_pipeline_url,
@ -69,6 +74,11 @@ describe QA::Support::Formatters::TestStatsFormatter do
RSpec::Core::Sandbox.sandboxed do |config|
config.formatter = QA::Support::Formatters::TestStatsFormatter
config.append_after do |example|
example.metadata[:api_fabrication] = Thread.current[:api_fabrication]
example.metadata[:browser_ui_fabrication] = Thread.current[:browser_ui_fabrication]
end
config.before(:context) { RSpec.current_example = nil }
example.run
@ -171,5 +181,21 @@ describe QA::Support::Formatters::TestStatsFormatter do
expect(influx_write_api).to have_received(:write).with(data: [data])
end
end
context 'with fabrication runtimes' do
let(:ui_fabrication) { 10 }
let(:api_fabrication) { 4 }
before do
Thread.current[:api_fabrication] = api_fabrication
Thread.current[:browser_ui_fabrication] = ui_fabrication
end
it 'exports data to influxdb with fabrication times' do
run_spec
expect(influx_write_api).to have_received(:write).with(data: [data])
end
end
end
end

59
scripts/changed-feature-flags Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'yaml'
require 'optparse'
require_relative 'api/default_options'
# This script returns the desired feature flag state as a comma-separated string for the feature flags in the specified files.
# Each desired feature flag state is specified as 'feature-flag=state'.
#
# For example, if the specified files included `config/feature_flags/development/ci_yaml_limit_size.yml` and the desired
# state as specified by the second argument was enabled, the value returned would be `ci_yaml_limit_size=enabled`
class GetFeatureFlagsFromFiles
def initialize(options)
@files = options.delete(:files)
@state = options.delete(:state)
end
def extracted_flags
files.each_with_object([]) do |file_path, all|
next unless file_path =~ %r{/feature_flags/development/.*\.yml}
next unless File.exist?(file_path)
ff_yaml = YAML.safe_load(File.read(file_path))
ff_to_add = "#{ff_yaml['name']}"
ff_to_add += "=#{state}" unless state.to_s.empty?
all << ff_to_add
end.join(',')
end
private
attr_reader :files, :state
end
if $0 == __FILE__
options = API::DEFAULT_OPTIONS.dup
OptionParser.new do |opts|
opts.on("-f", "--files FILES", Array, "Comma-separated list of feature flag config files") do |value|
options[:files] = value
end
opts.on("-s", "--state STATE", String,
"The desired state of the feature flags (enabled or disabled). If not specified the output will only list the feature flags."
) do |value|
options[:state] = value
end
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end.parse!
puts GetFeatureFlagsFromFiles.new(options).extracted_flags
end

View File

@ -154,7 +154,8 @@ module Trigger
'SECURITY_SOURCES' => Trigger.security? ? 'true' : 'false',
'ee' => Trigger.ee? ? 'true' : 'false',
'QA_BRANCH' => ENV['QA_BRANCH'] || 'master',
'CACHE_UPDATE' => ENV['OMNIBUS_GITLAB_CACHE_UPDATE']
'CACHE_UPDATE' => ENV['OMNIBUS_GITLAB_CACHE_UPDATE'],
'GITLAB_QA_OPTIONS' => ENV['GITLAB_QA_OPTIONS']
}
end
end

View File

@ -0,0 +1,88 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::AgentAuthorizationsFinder do
describe '#execute' do
let_it_be(:top_level_group) { create(:group) }
let_it_be(:subgroup1) { create(:group, parent: top_level_group) }
let_it_be(:subgroup2) { create(:group, parent: subgroup1) }
let_it_be(:bottom_level_group) { create(:group, parent: subgroup2) }
let_it_be(:agent_configuration_project) { create(:project, namespace: subgroup1) }
let_it_be(:requesting_project, reload: true) { create(:project, namespace: bottom_level_group) }
let_it_be(:staging_agent) { create(:cluster_agent, project: agent_configuration_project) }
let_it_be(:production_agent) { create(:cluster_agent, project: agent_configuration_project) }
subject { described_class.new(requesting_project).execute }
describe 'project authorizations' do
context 'agent configuration project does not share a root namespace with the given project' do
let(:unrelated_agent) { create(:cluster_agent) }
before do
create(:agent_project_authorization, agent: unrelated_agent, project: requesting_project)
end
it { is_expected.to be_empty }
end
context 'with project authorizations present' do
let!(:authorization) {create(:agent_project_authorization, agent: production_agent, project: requesting_project) }
it { is_expected.to match_array [authorization] }
end
context 'with overlapping authorizations' do
let!(:agent) { create(:cluster_agent, project: requesting_project) }
let!(:project_authorization) { create(:agent_project_authorization, agent: agent, project: requesting_project) }
let!(:group_authorization) { create(:agent_group_authorization, agent: agent, group: bottom_level_group) }
it { is_expected.to match_array [project_authorization] }
end
end
describe 'implicit authorizations' do
let!(:associated_agent) { create(:cluster_agent, project: requesting_project) }
it 'returns authorizations for agents directly associated with the project' do
expect(subject.count).to eq(1)
authorization = subject.first
expect(authorization).to be_a(Clusters::Agents::ImplicitAuthorization)
expect(authorization.agent).to eq(associated_agent)
end
end
describe 'authorized groups' do
context 'agent configuration project is outside the requesting project hierarchy' do
let(:unrelated_agent) { create(:cluster_agent) }
before do
create(:agent_group_authorization, agent: unrelated_agent, group: top_level_group)
end
it { is_expected.to be_empty }
end
context 'multiple agents are authorized for the same group' do
let!(:staging_auth) { create(:agent_group_authorization, agent: staging_agent, group: bottom_level_group) }
let!(:production_auth) { create(:agent_group_authorization, agent: production_agent, group: bottom_level_group) }
it 'returns authorizations for all agents' do
expect(subject).to contain_exactly(staging_auth, production_auth)
end
end
context 'a single agent is authorized to more than one matching group' do
let!(:bottom_level_auth) { create(:agent_group_authorization, agent: production_agent, group: bottom_level_group) }
let!(:top_level_auth) { create(:agent_group_authorization, agent: production_agent, group: top_level_group) }
it 'picks the authorization for the closest group to the requesting project' do
expect(subject).to contain_exactly(bottom_level_auth)
end
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::DeployableAgentsFinder do
describe '#execute' do
let_it_be(:agent) { create(:cluster_agent) }
let(:project) { agent.project }
subject { described_class.new(project).execute }
it { is_expected.to contain_exactly(agent) }
end
end

View File

@ -159,8 +159,9 @@ describe('LabelsSelect Mutations', () => {
labels = [
{ id: 1, title: 'scoped' },
{ id: 2, title: 'scoped::one', set: false },
{ id: 3, title: 'scoped::test', set: true },
{ id: 4, title: '' },
{ id: 3, title: 'scoped::two', set: false },
{ id: 4, title: 'scoped::three', set: true },
{ id: 5, title: '' },
];
});
@ -191,8 +192,9 @@ describe('LabelsSelect Mutations', () => {
expect(state.labels).toEqual([
{ id: 1, title: 'scoped' },
{ id: 2, title: 'scoped::one', set: true, touched: true },
{ id: 3, title: 'scoped::test', set: false },
{ id: 4, title: '' },
{ id: 3, title: 'scoped::two', set: false },
{ id: 4, title: 'scoped::three', set: false },
{ id: 5, title: '' },
]);
});
});

View File

@ -25,6 +25,7 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry do
"quota is exceeded" | 0 | { max: 2 } | :ci_quota_exceeded | false
"no matching runner" | 0 | { max: 2 } | :no_matching_runner | false
"missing dependencies" | 0 | { max: 2 } | :missing_dependency_failure | false
"forward deployment failure" | 0 | { max: 2 } | :forward_deployment_failure | false
end
with_them do

View File

@ -6,7 +6,23 @@ RSpec.describe Gitlab::Zentao::Client do
subject(:integration) { described_class.new(zentao_integration) }
let(:zentao_integration) { create(:zentao_integration) }
let(:mock_get_products_url) { integration.send(:url, "products/#{zentao_integration.zentao_product_xid}") }
def mock_get_products_url
integration.send(:url, "products/#{zentao_integration.zentao_product_xid}")
end
def mock_fetch_issue_url(issue_id)
integration.send(:url, "issues/#{issue_id}")
end
let(:mock_headers) do
{
headers: {
'Content-Type' => 'application/json',
'Token' => zentao_integration.api_token
}
}
end
describe '#new' do
context 'if integration is nil' do
@ -25,15 +41,6 @@ RSpec.describe Gitlab::Zentao::Client do
end
describe '#fetch_product' do
let(:mock_headers) do
{
headers: {
'Content-Type' => 'application/json',
'Token' => zentao_integration.api_token
}
}
end
context 'with valid product' do
let(:mock_response) { { 'id' => zentao_integration.zentao_product_xid } }
@ -54,7 +61,9 @@ RSpec.describe Gitlab::Zentao::Client do
end
it 'fetches the empty product' do
expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq({})
expect do
integration.fetch_product(zentao_integration.zentao_product_xid)
end.to raise_error(Gitlab::Zentao::Client::Error)
end
end
@ -65,21 +74,14 @@ RSpec.describe Gitlab::Zentao::Client do
end
it 'fetches the empty product' do
expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq({})
expect do
integration.fetch_product(zentao_integration.zentao_product_xid)
end.to raise_error(Gitlab::Zentao::Client::Error)
end
end
end
describe '#ping' do
let(:mock_headers) do
{
headers: {
'Content-Type' => 'application/json',
'Token' => zentao_integration.api_token
}
}
end
context 'with valid resource' do
before do
WebMock.stub_request(:get, mock_get_products_url)
@ -102,4 +104,29 @@ RSpec.describe Gitlab::Zentao::Client do
end
end
end
describe '#fetch_issue' do
context 'with invalid id' do
let(:invalid_ids) { ['story', 'story-', '-', '123', ''] }
it 'returns empty object' do
invalid_ids.each do |id|
expect { integration.fetch_issue(id) }.to raise_error(Gitlab::Zentao::Client::Error)
end
end
end
context 'with valid id' do
let(:valid_ids) { %w[story-1 bug-23] }
it 'fetches current issue' do
valid_ids.each do |id|
WebMock.stub_request(:get, mock_fetch_issue_url(id))
.with(mock_headers).to_return(status: 200, body: { issue: { id: id } }.to_json)
expect(integration.fetch_issue(id).dig('issue', 'id')).to eq id
end
end
end
end
end

View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Zentao::Query do
let(:zentao_integration) { create(:zentao_integration) }
let(:params) { {} }
subject(:query) { described_class.new(zentao_integration, ActionController::Parameters.new(params)) }
describe '#issues' do
let(:response) { { 'page' => 1, 'total' => 0, 'limit' => 20, 'issues' => [] } }
def expect_query_option_include(expected_params)
expect_next_instance_of(Gitlab::Zentao::Client) do |client|
expect(client).to receive(:fetch_issues)
.with(hash_including(expected_params))
.and_return(response)
end
query.issues
end
context 'when params are empty' do
it 'fills default params' do
expect_query_option_include(status: 'opened', order: 'lastEditedDate_desc', labels: '')
end
end
context 'when params contain valid options' do
let(:params) { { state: 'closed', sort: 'created_asc', labels: %w[Bugs Features] } }
it 'fills params with standard of ZenTao' do
expect_query_option_include(status: 'closed', order: 'openedDate_asc', labels: 'Bugs,Features')
end
end
context 'when params contain invalid options' do
let(:params) { { state: 'xxx', sort: 'xxx', labels: %w[xxx] } }
it 'fills default params with standard of ZenTao' do
expect_query_option_include(status: 'opened', order: 'lastEditedDate_desc', labels: 'xxx')
end
end
end
describe '#issue' do
let(:response) { { 'issue' => { 'id' => 'story-1' } } }
before do
expect_next_instance_of(Gitlab::Zentao::Client) do |client|
expect(client).to receive(:fetch_issue)
.and_return(response)
end
end
it 'returns issue object by client' do
expect(query.issue).to include('id' => 'story-1')
end
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::ZentaoMenu do
it_behaves_like 'ZenTao menu with CE version'
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('add_timestamp_softwarelicensespolicy')
require_migration!
RSpec.describe AddTimestampSoftwarelicensespolicy do
let(:software_licenses_policy) { table(:software_license_policies) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('backfill_project_settings')
require_migration!
RSpec.describe BackfillProjectSettings, :sidekiq, schema: 20200114113341 do
let(:projects) { table(:projects) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('remove_invalid_jira_data')
require_migration!
RSpec.describe RemoveInvalidJiraData do
let(:jira_tracker_data) { table(:jira_tracker_data) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('remove_invalid_issue_tracker_data')
require_migration!
RSpec.describe RemoveInvalidIssueTrackerData do
let(:issue_tracker_data) { table(:issue_tracker_data) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('reschedule_migrate_issue_trackers_data')
require_migration!
RSpec.describe RescheduleMigrateIssueTrackersData do
let(:services) { table(:services) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('remove_orphaned_chat_names')
require_migration!
RSpec.describe RemoveOrphanedChatNames, schema: 20200313202430 do
let(:projects) { table(:projects) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('backfill_deployment_clusters_from_deployments')
require_migration!
RSpec.describe BackfillDeploymentClustersFromDeployments, :migration, :sidekiq, schema: 20200227140242 do
describe '#up' do

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('change_variable_interpolation_format_in_common_metrics')
require_migration!
RSpec.describe ChangeVariableInterpolationFormatInCommonMetrics, :migration do
let(:prometheus_metrics) { table(:prometheus_metrics) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('dedup_mr_metrics')
require_migration!
RSpec.describe DedupMrMetrics, :migration, schema: 20200526013844 do
let(:namespaces) { table(:namespaces) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('update_index_approval_rule_name_for_code_owners_rule_type')
require_migration!
RSpec.describe UpdateIndexApprovalRuleNameForCodeOwnersRuleType do
let(:migration) { described_class.new }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('backfill_namespace_settings')
require_migration!
RSpec.describe BackfillNamespaceSettings, :sidekiq, schema: 20200703124823 do
let(:namespaces) { table(:namespaces) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('adjust_unique_index_alert_management_alerts')
require_migration!
RSpec.describe AdjustUniqueIndexAlertManagementAlerts, :migration do
let(:migration) { described_class.new }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('replace_unique_index_on_cycle_analytics_stages')
require_migration!
RSpec.describe ReplaceUniqueIndexOnCycleAnalyticsStages, :migration, schema: 20200727142337 do
let(:namespaces) { table(:namespaces) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('add_o_auth_paths_to_protected_paths')
require_migration!
RSpec.describe AddOAuthPathsToProtectedPaths do
subject(:migration) { described_class.new }

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('create_missing_vulnerabilities_issue_links')
require_migration!
RSpec.describe CreateMissingVulnerabilitiesIssueLinks, :migration do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('schedule_migration_to_hashed_storage')
require_migration!
RSpec.describe ScheduleMigrationToHashedStorage, :sidekiq do
describe '#up' do

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('create_initial_versions_for_pre_versioning_terraform_states')
require_migration!
RSpec.describe CreateInitialVersionsForPreVersioningTerraformStates do
let(:namespace) { table(:namespaces).create!(name: 'terraform', path: 'terraform') }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('drop_backfill_jira_tracker_deployment_type_jobs')
require_migration!
RSpec.describe DropBackfillJiraTrackerDeploymentTypeJobs, :sidekiq, :redis, schema: 2020_10_14_205300 do
subject(:migration) { described_class.new }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('migrate_services_to_http_integrations')
require_migration!
RSpec.describe MigrateServicesToHttpIntegrations do
let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('backfill_jira_tracker_deployment_type2')
require_migration!
RSpec.describe BackfillJiraTrackerDeploymentType2, :sidekiq, schema: 20201028182809 do
let(:services) { table(:services) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('cleanup_transfered_projects_shared_runners')
require_migration!
RSpec.describe CleanupTransferedProjectsSharedRunners, :sidekiq, schema: 20201110161542 do
let(:namespaces) { table(:namespaces) }

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('schedule_remove_duplicate_vulnerabilities_findings')
require_migration!
RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings, :migration do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('schedule_recalculate_uuid_on_vulnerabilities_occurrences')
require_migration!
RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences, :migration do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('remove_duplicate_services2')
require_migration!
RSpec.describe RemoveDuplicateServices2 do
let_it_be(:namespaces) { table(:namespaces) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('alter_vsa_issue_first_mentioned_in_commit_value')
require_migration!
RSpec.describe AlterVsaIssueFirstMentionedInCommitValue, schema: 20210114033715 do
let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('remove_bad_dependency_proxy_manifests')
require_migration!
RSpec.describe RemoveBadDependencyProxyManifests, schema: 20210128140157 do
let_it_be(:namespaces) { table(:namespaces) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('backfill_updated_at_after_repository_storage_move')
require_migration!
RSpec.describe BackfillUpdatedAtAfterRepositoryStorageMove, :sidekiq do
let_it_be(:projects) { table(:projects) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('add_environment_scope_to_group_variables')
require_migration!
RSpec.describe AddEnvironmentScopeToGroupVariables do
let(:migration) { described_class.new }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('dedup_issue_metrics')
require_migration!
RSpec.describe DedupIssueMetrics, :migration, schema: 20210205104425 do
let(:namespaces) { table(:namespaces) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('backfill_total_tuple_count_for_batched_migrations')
require_migration!
RSpec.describe BackfillTotalTupleCountForBatchedMigrations, :migration, schema: 20210406140057 do
let_it_be(:table_name) { 'projects' }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
require_migration!('reschedule_artifact_expiry_backfill_again')
require_migration!
RSpec.describe RescheduleArtifactExpiryBackfillAgain, :migration do
let(:migration_class) { Gitlab::BackgroundMigration::BackfillArtifactExpiryDate }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('schedule_update_jira_tracker_data_deployment_type_based_on_url')
require_migration!
RSpec.describe ScheduleUpdateJiraTrackerDataDeploymentTypeBasedOnUrl, :migration do
let(:services_table) { table(:services) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('schedule_drop_invalid_vulnerabilities')
require_migration!
RSpec.describe ScheduleDropInvalidVulnerabilities, :migration do
let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }

View File

@ -2,7 +2,7 @@
#
require 'spec_helper'
require_migration!('copy_adoption_snapshot_namespace')
require_migration!
RSpec.describe CopyAdoptionSnapshotNamespace, :migration, schema: 20210430124630 do
let(:namespaces_table) { table(:namespaces) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
require_migration!('copy_adoption_segments_namespace')
require_migration!
RSpec.describe CopyAdoptionSegmentsNamespace, :migration do
let(:namespaces_table) { table(:namespaces) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
require_migration!('add_project_value_stream_id_to_project_stages')
require_migration!
RSpec.describe AddProjectValueStreamIdToProjectStages, schema: 20210503105022 do
let(:stages) { table(:analytics_cycle_analytics_project_stages) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('schedule_drop_invalid_vulnerabilities2')
require_migration!
RSpec.describe ScheduleDropInvalidVulnerabilities2, :migration do
let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('schedule_cleanup_orphaned_lfs_objects_projects')
require_migration!
RSpec.describe ScheduleCleanupOrphanedLfsObjectsProjects, schema: 20210511165250 do
let(:lfs_objects_projects) { table(:lfs_objects_projects) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('fix_total_stage_in_vsa')
require_migration!
RSpec.describe FixTotalStageInVsa, :migration, schema: 20210518001450 do
let(:namespaces) { table(:namespaces) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('group_protected_environments_add_index_and_constraint')
require_migration!
RSpec.describe GroupProtectedEnvironmentsAddIndexAndConstraint do
let(:migration) { described_class.new }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
require_migration!('remove_builds_email_service_from_services')
require_migration!
RSpec.describe RemoveBuildsEmailServiceFromServices do
let(:namespaces) { table(:namespaces) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
require_migration!('delete_legacy_operations_feature_flags')
require_migration!
RSpec.describe DeleteLegacyOperationsFeatureFlags do
let(:namespace) { table(:namespaces).create!(name: 'foo', path: 'bar') }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
require_migration!('cascade_delete_freeze_periods')
require_migration!
RSpec.describe CascadeDeleteFreezePeriods do
let(:namespace) { table(:namespaces).create!(name: 'deploy_freeze', path: 'deploy_freeze') }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration! 'reschedule_merge_request_diff_users_background_migration'
require_migration!
RSpec.describe RescheduleMergeRequestDiffUsersBackgroundMigration, :migration do
let(:migration) { described_class.new }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('update_issuable_slas_where_issue_closed')
require_migration!
RSpec.describe UpdateIssuableSlasWhereIssueClosed, :migration do
let(:namespaces) { table(:namespaces) }

Some files were not shown because too many files have changed in this diff Show More