Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-06-20 18:08:32 +00:00
parent a5d7e614fe
commit 995bcca3fc
45 changed files with 590 additions and 878 deletions

View File

@ -109,7 +109,6 @@ linters:
# These cops should eventually get enabled
- Cop/LineBreakAfterGuardClauses
- Cop/ProjectPathHelper
- Gitlab/FeatureAvailableUsage
- Gitlab/Json
- GitlabSecurity/PublicSend
- Layout/EmptyLineAfterGuardClause

View File

@ -38,7 +38,6 @@ Gitlab/FeatureAvailableUsage:
- 'ee/app/models/approval_state.rb'
- 'ee/app/models/concerns/ee/approvable.rb'
- 'ee/app/models/concerns/ee/project_security_scanners_information.rb'
- 'ee/app/models/concerns/ee/protected_ref_access.rb'
- 'ee/app/models/concerns/insights_feature.rb'
- 'ee/app/models/ee/board.rb'
- 'ee/app/models/ee/ci/build.rb'
@ -68,7 +67,6 @@ Gitlab/FeatureAvailableUsage:
- 'ee/app/services/ci/audit_variable_change_service.rb'
- 'ee/app/services/dashboard/projects/create_service.rb'
- 'ee/app/services/dashboard/projects/list_service.rb'
- 'ee/app/services/ee/alert_management/http_integrations/create_service.rb'
- 'ee/app/services/ee/audit_event_service.rb'
- 'ee/app/services/ee/boards/issues/list_service.rb'
- 'ee/app/services/ee/boards/lists/create_service.rb'
@ -91,6 +89,31 @@ Gitlab/FeatureAvailableUsage:
- 'ee/app/services/projects/mark_for_deletion_service.rb'
- 'ee/app/services/requirements_management/process_test_reports_service.rb'
- 'ee/app/services/security/store_scans_service.rb'
- 'ee/app/views/compliance_management/compliance_framework/_project_settings.html.haml.rb'
- 'ee/app/views/groups/epics/index.html.haml.rb'
- 'ee/app/views/product_analytics/_project_settings.html.haml.rb'
- 'ee/app/views/projects/_merge_request_settings.html.haml.rb'
- 'ee/app/views/projects/_merge_request_settings_description_text.html.haml.rb'
- 'ee/app/views/projects/_remove.html.haml.rb'
- 'ee/app/views/projects/audit_events/index.html.haml.rb'
- 'ee/app/views/projects/blob/_header_file_locks.html.haml.rb'
- 'ee/app/views/projects/branch_defaults/_branch_names_help.html.haml.rb'
- 'ee/app/views/projects/merge_requests/show.html.haml.rb'
- 'ee/app/views/projects/push_rules/_index.html.haml.rb'
- 'ee/app/views/projects/quality/test_cases/index.html.haml.rb'
- 'ee/app/views/projects/settings/_default_issue_template.html.haml.rb'
- 'ee/app/views/projects/settings/_restore.html.haml.rb'
- 'ee/app/views/projects/settings/ci_cd/_auto_rollback.html.haml.rb'
- 'ee/app/views/projects/settings/ci_cd/_pipeline_subscriptions.html.haml.rb'
- 'ee/app/views/projects/settings/merge_requests/_merge_request_approvals_settings.html.haml.rb'
- 'ee/app/views/projects/settings/operations/_status_page.html.haml.rb'
- 'ee/app/views/projects/settings/repository/_protected_branches.html.haml.rb'
- 'ee/app/views/protected_branches/ee/_code_owner_approval_form.html.haml.rb'
- 'ee/app/views/protected_branches/ee/_code_owner_approval_table.html.haml.rb'
- 'ee/app/views/protected_branches/ee/_code_owner_approval_table_head.html.haml.rb'
- 'ee/app/views/shared/labels/_create_label_help_text.html.haml.rb'
- 'ee/app/views/shared/promotions/_promote_mr_features.html.haml.rb'
- 'ee/app/views/shared/promotions/_promote_repository_features.html.haml.rb'
- 'ee/app/workers/analytics/code_review_metrics_worker.rb'
- 'ee/app/workers/group_saml_group_sync_worker.rb'
- 'ee/lib/ee/api/entities/approval_state.rb'

View File

@ -687,7 +687,6 @@ Layout/EmptyLineAfterMagicComment:
- 'spec/requests/lfs_http_spec.rb'
- 'spec/rubocop/cop/migration/complex_indexes_require_name_spec.rb'
- 'spec/rubocop/cop/migration/refer_to_index_by_name_spec.rb'
- 'spec/rubocop/formatter/todo_formatter_spec.rb'
- 'spec/scripts/lib/glfm/parse_examples_spec.rb'
- 'spec/scripts/lib/glfm/shared_spec.rb'
- 'spec/scripts/lib/glfm/update_example_snapshots_spec.rb'

View File

@ -5370,9 +5370,7 @@ RSpec/MissingFeatureCategory:
- 'spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb'
- 'spec/rubocop/cop/usage_data/large_table_spec.rb'
- 'spec/rubocop/cop/user_admin_spec.rb'
- 'spec/rubocop/cop_todo_spec.rb'
- 'spec/rubocop/formatter/graceful_formatter_spec.rb'
- 'spec/rubocop/formatter/todo_formatter_spec.rb'
- 'spec/rubocop/migration_helpers_spec.rb'
- 'spec/rubocop/qa_helpers_spec.rb'
- 'spec/rubocop/todo_dir_spec.rb'

View File

@ -1,6 +1,6 @@
<script>
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
import { sprintf, __, formatNumber } from '~/locale';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
@ -49,6 +49,12 @@ export default {
managersCount() {
return this.runner.managers?.count || 0;
},
firstIpAddress() {
return this.runner.managers?.nodes?.[0]?.ipAddress || null;
},
additionalIpAddressCount() {
return this.managersCount - 1;
},
jobCount() {
return formatJobCount(this.runner.jobCount);
},
@ -63,6 +69,9 @@ export default {
return null;
},
},
methods: {
formatNumber,
},
i18n: {
I18N_NO_DESCRIPTION,
I18N_LOCKED_RUNNER_DESCRIPTION,
@ -120,8 +129,11 @@ export default {
</gl-sprintf>
</runner-summary-field>
<runner-summary-field v-if="runner.ipAddress" icon="disk" :tooltip="__('IP Address')">
{{ runner.ipAddress }}
<runner-summary-field v-if="firstIpAddress" icon="disk" :tooltip="__('IP Address')">
{{ firstIpAddress }}
<template v-if="additionalIpAddressCount"
>(+{{ formatNumber(additionalIpAddressCount) }})</template
>
</runner-summary-field>
<runner-summary-field icon="pipeline" data-testid="job-count" :tooltip="__('Jobs')">

View File

@ -6,7 +6,6 @@ fragment ListItemShared on CiRunner {
runnerType
shortSha
version
ipAddress
paused
locked
jobCount
@ -22,8 +21,11 @@ fragment ListItemShared on CiRunner {
updateRunner
deleteRunner
}
managers {
managers(first: 1) {
count
nodes {
ipAddress
}
}
groups(first: 1) {
nodes {

View File

@ -11,6 +11,7 @@ export const initMrMoreDropdown = () => {
const {
mergeRequest,
projectPath,
url,
editUrl,
isCurrentUser,
isLoggedIn,
@ -20,7 +21,6 @@ export const initMrMoreDropdown = () => {
sourceProjectMissing,
clipboardText,
reportedUserId,
reportedFromUrl,
} = el.dataset;
let mr;
@ -41,6 +41,7 @@ export const initMrMoreDropdown = () => {
props: {
mr,
projectPath,
url,
editUrl,
isCurrentUser,
isLoggedIn: Boolean(isLoggedIn),
@ -50,7 +51,6 @@ export const initMrMoreDropdown = () => {
sourceProjectMissing,
clipboardText,
reportedUserId: Number(reportedUserId),
reportedFromUrl,
},
}),
});

View File

@ -71,6 +71,11 @@ export default {
default: '',
required: false,
},
url: {
type: String,
default: '',
required: false,
},
editUrl: {
type: String,
default: '',
@ -116,11 +121,6 @@ export default {
default: 0,
required: false,
},
reportedFromUrl: {
type: String,
default: '',
required: false,
},
},
data() {
return {
@ -156,7 +156,7 @@ export default {
this.isLoadingDraft = true;
axios
.put(`?merge_request[wip_event]=${this.draftState}`, null, {
.put(`${this.url}?merge_request[wip_event]=${this.draftState}`, null, {
params: { format: 'json' },
})
.then(({ data }) => {
@ -353,7 +353,7 @@ export default {
<abuse-category-selector
v-if="!isCurrentUser && isReportAbuseDrawerOpen"
:reported-user-id="reportedUserId"
:reported-from-url="reportedFromUrl"
:reported-from-url="url"
:show-drawer="isReportAbuseDrawerOpen"
@close-drawer="reportAbuseAction(false)"
/>

View File

@ -31,7 +31,6 @@
&:not(.note-form).internal-note .timeline-content,
&:not(.note-form).draft-note .timeline-content {
background-color: $orange-50 !important;
border-radius: 3px;
}
.timeline-entry-inner {

View File

@ -43,3 +43,15 @@
.branches-list .branch-item:not(:last-of-type) {
border-bottom: 1px solid $border-color;
}
.branch-item {
.issuable-reference {
max-width: 92px;
}
.right-block {
@media (min-width: map-get($grid-breakpoints, md)) {
min-width: 200px;
}
}
}

View File

@ -63,20 +63,6 @@ class PlanLimits < ApplicationRecord
update(limits_history: limits_history)
end
def limit_attribute_changes(attribute)
limit_history = limits_history[attribute]
return [] unless limit_history
limit_history.map do |entry|
{
timestamp: entry[:timestamp],
value: entry[:value],
username: entry[:username],
user_id: entry[:user_id]
}
end
end
end
PlanLimits.prepend_mod_with('PlanLimits')

View File

@ -17,7 +17,7 @@
= render_if_exists 'projects/branches/diverged_from_upstream', branch: branch
.block-truncated
.gl-text-truncate
- if commit
= render 'projects/branches/commit', commit: commit, project: @project
- else
@ -28,35 +28,33 @@
.pipeline-status.d-none.d-md-block<
- if commit_status
= render 'ci/status/icon', size: 16, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5'
= render 'ci/status/icon', size: 16, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-3'
- elsif show_commit_status
.gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5
.gl-display-inline-flex.gl-vertical-align-middle.gl-mr-3
%svg.s16
.right-block.gl-display-flex.gl-align-items-center.gl-justify-content-end
.gl-mr-3
- if mr_status.present?
.issuable-reference.gl-display-flex.gl-justify-content-end.gl-overflow-hidden
= gl_badge_tag issuable_reference(related_merge_request),
{ icon: mr_status[:icon], variant: mr_status[:variant], size: :md, href: merge_request_path(related_merge_request) },
{ class: 'gl-display-block gl-text-truncate', title: mr_status[:title], data: { toggle: 'tooltip', container: 'body', qa_selector: 'badge_content' } }
- if mr_status.present?
.issuable-reference.gl-display-flex.gl-justify-content-end.gl-min-w-10.gl-ml-5.gl-mr-4
= gl_badge_tag issuable_reference(related_merge_request),
{ icon: mr_status[:icon], variant: mr_status[:variant], size: :md, href: merge_request_path(related_merge_request) },
{ class: 'gl-mr-2', title: mr_status[:title], data: { toggle: 'tooltip', container: 'body', qa_selector: 'badge_content' } }
- elsif mr_status.nil? && create_mr_button?(from: branch.name, source_project: @project)
= render Pajamas::ButtonComponent.new(icon: 'merge-request', href: create_mr_path(from: branch.name, source_project: @project), button_options: { class: 'has-tooltip', title: _('New merge request') }) do
= _('New')
.controls.d-none.d-md-block<
- if mr_status.nil? && create_mr_button?(from: branch.name, source_project: @project)
= render Pajamas::ButtonComponent.new(icon: 'merge-request', href: create_mr_path(from: branch.name, source_project: @project), button_options: { class: 'has-tooltip gl-mr-2!', title: _('New merge request') }) do
= _('New')
= render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name], css_class: 'gl-mr-2!'
= render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name], css_class: 'gl-mr-1!'
- if !is_default_branch
.js-branch-more-actions{ data: {
branch_name: branch.name,
default_branch_name: @repository.root_ref,
can_delete_branch: user_access(@project).can_delete_branch?(branch.name).to_s,
is_protected_branch: protected_branch?(@project, branch).to_s,
merged: merged.to_s,
compare_path: project_compare_index_path(@project, from: @repository.root_ref, to: branch.name),
delete_path: project_branch_path(@project, branch.name),
} }
- else
.gl-display-inline-flex.gl-w-7
&nbsp;
.gl-w-7
- if !is_default_branch
.js-branch-more-actions{ data: {
branch_name: branch.name,
default_branch_name: @repository.root_ref,
can_delete_branch: user_access(@project).can_delete_branch?(branch.name).to_s,
is_protected_branch: protected_branch?(@project, branch).to_s,
merged: merged.to_s,
compare_path: project_compare_index_path(@project, from: @repository.root_ref, to: branch.name),
delete_path: project_branch_path(@project, branch.name),
} }

View File

@ -1,7 +1,7 @@
.branch-commit.gl-font-sm.gl-text-gray-500
.branch-commit.gl-font-sm.gl-text-gray-500.gl-text-truncate
= link_to commit.short_id, project_commit_path(project, commit.id), class: "commit-sha"
&middot;
%span.str-truncated
%span
= link_to_markdown commit.title, project_commit_path(project, commit.id), class: "commit-row-message gl-text-gray-500!"
&middot;
%span.gl-text-secondary= time_ago_with_tooltip(commit.committed_date)

View File

@ -31,7 +31,7 @@
- if can_push_code
= link_to new_project_branch_path(@project), class: 'gl-button btn btn-confirm' do
= s_('Branches|New branch')
.js-delete-merged-branches{ data: {
.js-delete-merged-branches.gl-w-7{ data: {
default_branch: @project.repository.root_ref,
form_path: project_merged_branches_path(@project) }
}

View File

@ -1,6 +1,7 @@
.js-mr-more-dropdown{ data: {
merge_request: @merge_request.to_json,
project_path: @project.full_path,
url: merge_request_url(@merge_request),
edit_url: edit_project_merge_request_path(@project, @merge_request),
is_current_user: issuable_author_is_current_user(@merge_request),
is_logged_in: current_user,
@ -11,5 +12,4 @@
clipboard_text: @merge_request.to_reference(full: true),
report_abuse_path: add_category_abuse_reports_path,
reported_user_id: @merge_request.author.id,
reported_from_url: merge_request_url(@merge_request),
} }

View File

@ -1,8 +0,0 @@
---
name: ci_includable_files_interpolation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113211
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/399146
milestone: '15.11'
type: development
group: group::pipeline authoring
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: phone_verification_for_low_risk_users
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124090
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/415674
milestone: '16.2'
type: experiment
group: group::anti-abuse
default_enabled: false

View File

@ -4,6 +4,23 @@ require 'gitlab/redis'
Redis.raise_deprecations = true unless Rails.env.production?
# rubocop:disable Gitlab/NoCodeCoverageComment
# :nocov: This snippet is for local development only, reloading in specs would raise NameError
if Rails.env.development?
# reset all pools in the event of a reload
# This makes sure that there are no stale references to classes in the `Gitlab::Redis` namespace
# that also got reloaded.
Gitlab::Application.config.to_prepare do
Gitlab::Redis::ALL_CLASSES.each do |redis_instance|
redis_instance.instance_variable_set(:@pool, nil)
end
Rails.cache = ActiveSupport::Cache::RedisCacheStore.new(**Gitlab::Redis::Cache.active_support_config)
end
end
# :nocov:
# rubocop:enable Gitlab/NoCodeCoverageComment
Redis::Client.prepend(Gitlab::Instrumentation::RedisInterceptor)
Redis::Cluster::NodeLoader.prepend(Gitlab::Patch::NodeLoader)
Redis::Cluster.prepend(Gitlab::Patch::RedisCluster)

View File

@ -435,7 +435,7 @@ GET /projects/:id/releases/:tag_name/downloads/:direct_asset_path
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/downloads/bin/asset.exe"
curl --location --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/downloads/bin/asset.exe"
```
### Get the latest release

View File

@ -1,11 +1,11 @@
---
status: proposed
status: implemented
creation-date: "2022-11-23"
authors: [ "@shinya.maeda" ]
coach: "@DylanGriffith"
approvers: [ "@nagyv-gitlab", "@cbalane", "@hustewart", "@hfyngvason" ]
owning-stage: "~devops::release"
participating-stages: [Configure, Release]
owning-stage: "~devops::deploy"
participating-stages: [Environments]
---
<!-- vale gitlab.FutureTense = NO -->
@ -29,8 +29,6 @@ This blueprint describes how the association is established and how these domain
- The cluster resources and events can be visualized per [GitLab Environment](../../../ci/environments/index.md).
An environment-specific view scoped to the resources managed either directly or indirectly by a deployment commit.
- Support both [GitOps mode](../../../user/clusters/agent/gitops.md#gitops-configuration-reference) and [CI Access mode](../../../user/clusters/agent/ci_cd_workflow.md#authorize-the-agent).
- NOTE: At the moment, we focus on the solution for CI Access mode. GitOps mode will have significant architectural changes _outside of_ this blueprint,
such as [Flux switching](https://gitlab.com/gitlab-org/gitlab/-/issues/357947) and [Manifest projects outside of the Agent configuration project](https://gitlab.com/groups/gitlab-org/-/epics/7704). In order to derisk potential rework, we'll revisit the GitOps mode after these upstream changes have been settled.
### Non-Goals
@ -41,22 +39,22 @@ This blueprint describes how the association is established and how these domain
### Overview
- GitLab Environment and Agent-managed Resource Group have 1-to-1 relationship.
- Agent-managed Resource Group tracks all resources produced by the connected [agent](../../../user/clusters/agent/index.md). This includes not only resources written in manifest files but also subsequently generated resources (e.g. `Pod`s created by `Deployment` manifest file).
- Agent-managed Resource Group renders dependency graph, such as `Deployment` => `ReplicaSet` => `Pod`. This is for providing ArgoCD-style resource view.
- Agent-managed Resource Group has the Resource Health status that represents a summary of resource statuses, such as `Healthy`, `Progressing` or `Degraded`.
- GitLab Environment and GitLab Agent For Kubernetes have 1-to-1 relationship.
- GitLab Environment tracks all resources produced by the connected [agent](../../../user/clusters/agent/index.md). This includes not only resources written in manifest files but also subsequently generated resources (for example, `Pod`s created by `Deployment` manifest file).
- GitLab Environment renders dependency graph, such as `Deployment` => `ReplicaSet` => `Pod`. This is for providing ArgoCD-style resource view.
- GitLab Environment has the Resource Health status that represents a summary of resource statuses, such as `Healthy`, `Progressing` or `Degraded`.
```mermaid
flowchart LR
subgraph Kubernetes["Kubernetes"]
subgraph ResourceGroupProduction["ResourceGroup"]
subgraph ResourceGroupProduction["Production"]
direction LR
ResourceGroupProductionService(["Service"])
ResourceGroupProductionDeployment(["Deployment"])
ResourceGroupProductionPod1(["Pod1"])
ResourceGroupProductionPod2(["Pod2"])
end
subgraph ResourceGroupStaging["ResourceGroup"]
subgraph ResourceGroupStaging["Staging"]
direction LR
ResourceGroupStagingService(["Service"])
ResourceGroupStagingDeployment(["Deployment"])
@ -90,26 +88,18 @@ flowchart LR
- GitLab Project and Agent have 1-to-many _direct_ relationship. Only one project can own a specific agent.
- [GitOps mode](../../../user/clusters/agent/gitops.md#gitops-configuration-reference)
- GitLab Project and Agent do _NOT_ have many-to-many _indirect_ relationship yet. This will be supported in [Manifest projects outside of the Agent configuration project](https://gitlab.com/groups/gitlab-org/-/epics/7704).
- Agent and Agent-managed Resource Group have 1-to-1 relationship. Inventory IDs are used to group Kubernetes resources. This might be changed in [Flux switching](https://gitlab.com/gitlab-org/gitlab/-/issues/357947).
- [CI Access mode](../../../user/clusters/agent/ci_cd_workflow.md#authorize-the-agent)
- GitLab Project and Agent have many-to-many _indirect_ relationship. The project owning the agent can [share the access with the other proejcts](../../../user/clusters/agent/ci_cd_workflow.md#authorize-the-agent-to-access-projects-in-your-groups). (NOTE: Technically, only running jobs inside the project are allowed to access the cluster due to job-token authentication.)
- Agent and Agent-managed Resource Group do _NOT_ have relationships yet.
### Issues
- Agent-managed Resource Group should have environment ID as the foreign key, which must be unique across resource groups.
- Agent-managed Resource Group should have parameters how to group resources in the associated cluster, for example, `namespace`, `lable` and `inventory-id` (GitOps mode only) can passed as parameters.
- Agent-managed Resource Group should be able to fetch all relevant resources, including both default resource kinds and other [Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/).
- Agent-managed Resource Group should be aware of dependency graph.
- Agent-managed Resource Group should be able to compute Resource Health status from the associated resources.
- GitLab Environment should have ID of GitLab Agent For Kubernetes as the foreign key.
- GitLab Environment should have parameters how to group resources in the associated cluster, for example, `namespace`, `lable` and `inventory-id` (GitOps mode only) can passed as parameters.
- GitLab Environment should be able to fetch all relevant resources, including both default resource kinds and other [Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/).
- GitLab Environment should be aware of dependency graph.
- GitLab Environment should be able to compute Resource Health status from the associated resources.
### Example: Pull-based deployment (GitOps mode)
NOTE:
At the moment, we focus on the solution for CI Access mode. GitOps mode will have significant architectural changes _outside of_ this blueprint,
such as [Flux switching](https://gitlab.com/gitlab-org/gitlab/-/issues/357947) and [Manifest projects outside of the Agent configuration project](https://gitlab.com/groups/gitlab-org/-/epics/7704). In order to derisk potential rework, we'll revisit the GitOps mode after these upstream changes have been settled.
### Example: Push-based deployment (CI access mode)
### Example
This is an example of how the architecture works in push-based deployment.
The feature is documented [here](../../../user/clusters/agent/ci_cd_workflow.md) as CI access mode.
@ -117,21 +107,21 @@ The feature is documented [here](../../../user/clusters/agent/ci_cd_workflow.md)
```mermaid
flowchart LR
subgraph ProductionKubernetes["Production Kubernetes"]
subgraph ResourceGroupProductionFrontend["ResourceGroup"]
subgraph ResourceGroupProductionFrontend["Production"]
direction LR
ResourceGroupProductionFrontendService(["Service"])
ResourceGroupProductionFrontendDeployment(["Deployment"])
ResourceGroupProductionFrontendPod1(["Pod1"])
ResourceGroupProductionFrontendPod2(["Pod2"])
end
subgraph ResourceGroupProductionBackend["ResourceGroup"]
subgraph ResourceGroupProductionBackend["Staging"]
direction LR
ResourceGroupProductionBackendService(["Service"])
ResourceGroupProductionBackendDeployment(["Deployment"])
ResourceGroupProductionBackendPod1(["Pod1"])
ResourceGroupProductionBackendPod2(["Pod2"])
end
subgraph ResourceGroupProductionPrometheus["ResourceGroup"]
subgraph ResourceGroupProductionPrometheus["Monitoring"]
direction LR
ResourceGroupProductionPrometheusService(["Service"])
ResourceGroupProductionPrometheusDeployment(["Deployment"])
@ -202,21 +192,21 @@ The microservice project setup can be improved by [Multi-Project Deployment Pipe
```mermaid
flowchart LR
subgraph ProductionKubernetes["Production Kubernetes"]
subgraph ResourceGroupProductionFrontend["ResourceGroup"]
subgraph ResourceGroupProductionFrontend["Frontend"]
direction LR
ResourceGroupProductionFrontendService(["Service"])
ResourceGroupProductionFrontendDeployment(["Deployment"])
ResourceGroupProductionFrontendPod1(["Pod1"])
ResourceGroupProductionFrontendPod2(["Pod2"])
end
subgraph ResourceGroupProductionBackend["ResourceGroup"]
subgraph ResourceGroupProductionBackend["Backend"]
direction LR
ResourceGroupProductionBackendService(["Service"])
ResourceGroupProductionBackendDeployment(["Deployment"])
ResourceGroupProductionBackendPod1(["Pod1"])
ResourceGroupProductionBackendPod2(["Pod2"])
end
subgraph ResourceGroupProductionPrometheus["ResourceGroup"]
subgraph ResourceGroupProductionPrometheus["Monitoring"]
direction LR
ResourceGroupProductionPrometheusService(["Service"])
ResourceGroupProductionPrometheusDeployment(["Deployment"])
@ -266,104 +256,18 @@ flowchart LR
DeploymentPipelines -- "Deploy" --> ResourceGroupProductionBackend
```
#### View all Agent-managed Resource Groups on production environment
At the group-level, we can accumulate all environments match a specific tier, for example,
listing all environments with `production` tier from subsequent projects.
This is useful to see the entire Agent-managed Resource Groups on production environment.
The following diagram examplifies the relationship between GitLab group and Kubernetes resources:
```mermaid
flowchart LR
subgraph Kubernetes["Kubernetes"]
subgraph ResourceGroupProduction["ResourceGroup"]
direction LR
ResourceGroupProductionService(["Service"])
ResourceGroupProductionDeployment(["Deployment"])
ResourceGroupProductionPod1(["Pod1"])
ResourceGroupProductionPod2(["Pod2"])
end
subgraph ResourceGroupStaging["ResourceGroup"]
direction LR
ResourceGroupStagingService(["Service"])
ResourceGroupStagingDeployment(["Deployment"])
ResourceGroupStagingPod1(["Pod1"])
ResourceGroupStagingPod2(["Pod2"])
end
end
subgraph GitLab
subgraph Organization
OrganizationProduction["All resources on production"]
subgraph Frontend project
FrontendEnvironmentProduction["production environment"]
end
subgraph Backend project
BackendEnvironmentProduction["production environment"]
end
end
end
FrontendEnvironmentProduction --- ResourceGroupProduction
BackendEnvironmentProduction --- ResourceGroupStaging
ResourceGroupProductionService -.- ResourceGroupProductionDeployment
ResourceGroupProductionDeployment -.- ResourceGroupProductionPod1
ResourceGroupProductionDeployment -.- ResourceGroupProductionPod2
ResourceGroupStagingService -.- ResourceGroupStagingDeployment
ResourceGroupStagingDeployment -.- ResourceGroupStagingPod1
ResourceGroupStagingDeployment -.- ResourceGroupStagingPod2
OrganizationProduction --- FrontendEnvironmentProduction
OrganizationProduction --- BackendEnvironmentProduction
```
A few notes:
- In the future, we'd have more granular filters for resource search.
For example, there are two environments `production/us-region` and `production/eu-region` in each project
and show only resources in US region at the group-level.
This could be achivable by query filtering in PostgreSQL or label/namespace filtering in Kubernetes.
- Please see [Add dynamically populated organization-level environments page](https://gitlab.com/gitlab-org/gitlab/-/issues/241506) for more information.
## Design and implementation details
NOTE:
The following solution might be only applicable for CI Access mode. GitOps mode will have significant architectural changes _outside of_ this blueprint,
such as [Flux switching](https://gitlab.com/gitlab-org/gitlab/-/issues/357947) and [Manifest projects outside of the Agent configuration project](https://gitlab.com/groups/gitlab-org/-/epics/7704). In order to derisk potential rework, we'll revisit the GitOps mode after these upstream changes have been settled.
### Associate Environment with Agent
As a preliminary step, we allow users to explicitly define "which deployment job" uses "which agent" and deploy to "which namespace". The following keywords are supported in `.gitlab-ci.yml`.
Users can explicitly set a GitLab Agent For Kubernetes to a GitLab Environment in setting UI.
Frontend will use this associated agent for authenticating/authorizing the user access, which is described in a latter section.
- `environment:kubernetes:agent` ... Define which agent the deployment job uses. It can select the appropriate context from the `KUBE_CONFIG`.
- `environment:kubernetes:namespace` ... Define which namespace the deployment job deploys to. It injects `KUBE_NAMESPACE` predefined variable into the job. This keyword already [exists](../../../ci/yaml/index.md#environmentkubernetes).
Here is an example of `.gitlab-ci.yml`.
```yaml
deploy-production:
environment:
name: production
kubernetes:
agent: path/to/agent/repository:agent-name
namespace: default
script:
- helm --context="$KUBE_CONTEXT" --namespace="$KUBE_NAMESPACE" upgrade --install
```
When a deployment job is created, GitLab persists the relationship of specified agent, namespace and deployment job. If the CI job is NOT authorized to access the agent (Please refer [`Clusters::Agents::FilterAuthorizationsService`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/clusters/agents/filter_authorizations_service.rb) for more details), this relationship aren't recorded. This process happens in [`Deployments::CreateForBuildService`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/deployments/create_for_build_service.rb). The database table scheme is:
```plaintext
agent_deployments:
- deployment_id (bigint/FK/NOT NULL/Unique)
- agent_id (bigint/FK/NOT NULL)
- kubernetes_namespace (character varying(255)/NOT NULL)
```
To idenfity an associated agent for a specific environment, `environment.last_deployment.agent` can be used in Rails.
We need to adjust the `read_cluster_agent` permission in DeclarivePolicy for supporting agents shared by an external project (also known as the Agent management project).
### Fetch resources through `user_access`
When user visits an environment page, GitLab frontend fetches an environment via GraphQL. Frontend additionally fetches the associated agent-ID and namespace through deployment relationship, which being tracked by the `agent_deployments` table.
When user visits an environment page, GitLab frontend fetches an environment via GraphQL. Frontend additionally fetches the associated agent-ID and namespace.
Here is an example of GraphQL query:
@ -373,12 +277,12 @@ Here is an example of GraphQL query:
id
environment(name: "<environment-name>") {
slug
lastDeployment(status: SUCCESS) {
agent {
id
kubernetesNamespace
clusterAgent {
id
name
project {
name
project
kubernetesNamespace
}
}
}
@ -388,12 +292,9 @@ Here is an example of GraphQL query:
GitLab frontend authenticate/authorize the user access with [browser cookie](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/kubernetes_user_access.md#browser-cookie-on-gitlab-frontend). If the access is forbidden, frontend shows an error message that `You don't have access to an agent that deployed to this environment. Please contact agent administrator if you are allowed in "user_access" in agent config file. See <troubleshooting-doc-link>`.
After the user gained access to the agent, GitLab frontend fetches available API Resource list in the Kubernetes and fetches the resources with the following parameters:
After the user gained access to the agent, GitLab frontend fetches specific Resource kinds (for example, `Deployment`, `Pod`) in the Kubernetes with the following parameters:
- `namespace` ... `#{environment.lastDeployment.agent.kubernetesNamespace}`
- `labels`
- `app.gitlab.com/project_id=#{project.id}` _AND_
- `app.gitlab.com/environment_slug: #{environment.slug}`
- `namespace` ... `#{environment.kubernetesNamespace}`
If no resources are found, this is likely that the users have not embedded these lables into their resources. In this case, frontend shows an warning message `There are no resources found for the environment. Do resources have GitLab preserved labels? See <troubleshooting-doc-link>`.

View File

@ -84,6 +84,43 @@ For features that use the embedding database, additional setup is needed.
1. Run `gdk reconfigure`
1. Run database migrations to create the embedding database
### Set up GitLab Chat
1. [Enable Anthropic API features](#configure-anthropic-access).
1. [Enable OpenAI support](#configure-openai-access).
1. [Ensure the embedding database is configured](#set-up-the-embedding-database).
1. Enable feature specific feature flag.
```ruby
Feature.enable(:gitlab_duo)
Feature.enable(:tanuki_bot)
Feature.enable(:ai_redis_cache)
```
1. Ensure that your current branch is up-to-date with `master`.
1. To access the GitLab Chat interface, in the lower-left corner of any page, select **Help** and **Ask GitLab Chat**.
#### Tips for local development
1. When responses are taking too long to appear in the user interface, consider restarting Sidekiq by running `gdk restart rails-background-jobs`. If that doesn't work, try `gdk kill` and then `gdk start`.
1. Alternatively, bypass Sidekiq entirely and run the chat service syncronously. This can help with debugging errors as GraphQL errors are now available in the network inspector instead of the Sidekiq logs.
```diff
diff --git a/ee/app/services/llm/chat_service.rb b/ee/app/services/llm/chat_service.rb
index 5fa7ae8a2bc1..5fe996ba0345 100644
--- a/ee/app/services/llm/chat_service.rb
+++ b/ee/app/services/llm/chat_service.rb
@@ -5,7 +5,7 @@ class ChatService < BaseService
private
def perform
- worker_perform(user, resource, :chat, options)
+ worker_perform(user, resource, :chat, options.merge(sync: true))
end
def valid?
```
### Setup for GitLab documentation chat (legacy chat)
To populate the embedding database for GitLab chat:

View File

@ -23,9 +23,9 @@ is the [lib/](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/) folder.
The **lib/** folder is a mix of code that is generic/universal, GitLab-specific, and tightly integrated with the rest of the codebase.
Ask yourself the question: **can this code also be used in a Ruby project other than the current project?**.
Ask yourself the question: **is this code generic or universal that can be done as a separate and small project?**.
If the answer is **Yes** you should strongly consider starting with creating a new Gem [in the same repo](#in-the-same-repo)
and eventually evaluate whether to extract it as a separate repository.
and eventually evaluate whether to extract it as a separate repository if its meant to be used by other projects.
## Advantages of using Gems

View File

@ -50,6 +50,32 @@ bundle exec guard
When using spring and guard together, use `SPRING=1 bundle exec guard` instead to make use of spring.
### General guidelines
- Use a single, top-level `RSpec.describe ClassName` block.
- Use `.method` to describe class methods and `#method` to describe instance
methods.
- Use `context` to test branching logic (`RSpec/AvoidConditionalStatements` Rubocop Cop - [MR](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117152)).
- Try to match the ordering of tests to the ordering in the class.
- Try to follow the [Four-Phase Test](https://thoughtbot.com/blog/four-phase-test) pattern, using newlines
to separate phases.
- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`.
- Don't assert against the absolute value of a sequence-generated attribute (see
[Gotchas](../gotchas.md#do-not-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
- Avoid using `expect_any_instance_of` or `allow_any_instance_of` (see
[Gotchas](../gotchas.md#avoid-using-expect_any_instance_of-or-allow_any_instance_of-in-rspec)).
- Don't supply the `:each` argument to hooks because it's the default.
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`.
- When using `evaluate_script("$('.js-foo').testSomething()")` (or `execute_script`) which acts on a given element,
use a Capybara matcher beforehand (such as `find('.js-foo')`) to ensure the element actually exists.
- Use `focus: true` to isolate parts of the specs you want to run.
- Use [`:aggregate_failures`](https://rspec.info/features/3-12/rspec-core/expectation-framework-integration/aggregating-failures/) when there is more than one expectation in a test.
- For [empty test description blocks](https://github.com/rubocop-hq/rspec-style-guide#it-and-specify), use `specify` rather than `it do` if the test is self-explanatory.
- Use `non_existing_record_id`/`non_existing_record_iid`/`non_existing_record_access_level`
when you need an ID/IID/access level that doesn't actually exist. Using 123, 1234,
or even 999 is brittle as these IDs could actually exist in the database in the
context of a CI run.
### Eager loading the application code
By default, the application code:
@ -132,9 +158,44 @@ We should reduce test dependencies, and avoiding
capabilities also reduces the amount of set-up needed.
`:js` is particularly important to avoid. This must only be used if the feature
test requires JavaScript reactivity in the browser. Using a headless
test requires JavaScript reactivity in the browser (e.g. clicking a Vue.js component). Using a headless
browser is much slower than parsing the HTML response from the app.
#### Profiling: see where your test spend its time
[`rspec-stackprof`](https://github.com/dkhroad/rspec-stackprof) can be used to generate a flame graph that shows you where you test spend its time.
The gem generates a JSON report that we can upload to <https://www.speedscope.app> for an interactive visualization.
##### Installation
`stackprof` gem is [already installed with GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/695fcee0c5541b4ead0a90eb9b8bf0b69bee796c/Gemfile#L487), and we also have a script available that generates the JSON report (`bin/rspec-stackprof`).
```shell
# Optional: install the `speedscope` package to easily upload the JSON report to https://www.speedscope.app
npm install -g speedscope
```
##### Generate the JSON report
```shell
bin/rspec-stackprof --speedscope=true <your_slow_spec>
# There will be the name of the report displayed when the script ends.
# Upload the JSON report to speedscope.app
speedscope tmp/<your-json-report>.json
```
##### How to interpret the flamegraph
Below are some useful tips to interpret and navigate the flamegraph:
- There are [several views available](https://github.com/jlfwong/speedscope#views) for the flamegraph. `Left Heavy` is particularly useful when there are a lot of function calls (e.g. feature specs).
- You can zoom in or out! See [the navigation documentation](https://github.com/jlfwong/speedscope#navigation)
- If you are working on a slow feature test, search for `Capybara::DSL#` in the search to see the capybara actions that are made, and how long they take!
See [#414929](https://gitlab.com/gitlab-org/gitlab/-/issues/414929#note_1425239887) or [#375004](https://gitlab.com/gitlab-org/gitlab/-/issues/375004#note_1109867718) for some analysis examples.
#### Optimize factory usage
A common cause of slow tests is excessive creation of objects, and thus
@ -240,9 +301,21 @@ There are various ways to create objects and store them in variables in your tes
- `let` lazily creates the object. It isn't created until the object is called. `let` is generally inefficient as it creates a new object for every example. `let` is fine for simple values. However, more efficient variants of `let` are best when dealing with database models such as factories.
- `let_it_be_with_refind` works similar to `let_it_be_with_reload`, but the [former calls `ActiveRecord::Base#find`](https://github.com/test-prof/test-prof/blob/936b29f87b36f88a134e064aa6d8ade143ae7a13/lib/test_prof/ext/active_record_refind.rb#L15) instead of `ActiveRecord::Base#reload`. `reload` is usually faster than `refind`.
- `let_it_be_with_reload` creates an object one time for all examples in the same context, but after each example, the database changes are rolled back, and `object.reload` will be called to restore the object to its original state. This means you can make changes to the object before or during an example. However, there are cases where [state leaks across other models](https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#state-leakage-detection) can occur. In these cases, `let` may be an easier option, especially if only a few examples exist.
- `let_it_be` creates an immutable object one time for all of the examples in the same context. This is a great alternative to `let` and `let!` for objects that do not need to change from one example to another. Using `let_it_be` can dramatically speed up tests that create database models. See <https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#let-it-be> for more details and examples.
- `let_it_be` creates an object one time for all of the examples in the same context. This is a great alternative to `let` and `let!` for objects that do not need to change from one example to another. Using `let_it_be` can dramatically speed up tests that create database models. See <https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#let-it-be> for more details and examples.
`let_it_be` is the most optimized option since it instantiates an object once and does not change it. If you find yourself needing `let` instead of `let_it_be`, try `let_it_be_with_reload`.
Pro-tip: When writing tests, it is best to consider the objects inside a `let_it_be` as **immutable**, as there are some important caveats when modifying objects inside a `let_it_be` declaration ([1](https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#database-is-rolled-back-to-a-pristine-state-but-the-objects-are-not), [2](https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#modifiers)). To make your `let_it_be` objects immutable, consider using `.freeze`:
```shell
# Before
let_it_be(:namespace) { create_default(:namespace)
# After
let_it_be(:namespace) { create_default(:namespace).freeze
```
See <https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#state-leakage-detection> for more information on `let_it_be` freezing.
`let_it_be` is the most optimized option since it instantiates an object once and shares its instance across examples. If you find yourself needing `let` instead of `let_it_be`, try `let_it_be_with_reload`.
```ruby
# Old
@ -336,6 +409,35 @@ NOTE:
Refrain from using this stub helper if the test code relies on persisting
`project_authorizations` or `Member` records. Use `Project#add_member` or `Group#add_member` instead.
#### Additional profiling metrics
We can use the `rspec_profiling` gem to diagnose, for instance, the number of SQL queries we're making when running a test.
This could be caused by some application side SQL queries **triggered by a test that could mock parts that are not under test** (e.g. [!123810](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123810)).
[See the instructions in the performance docs](../performance.md#rspec-profiling).
#### Troubleshoot slow feature test
A slow feature test can generally be optimized the same way as any other test. However, there are some specific techniques that can make the troubleshooting session more fruitful.
##### See what the feature test is doing in the UI
```shell
# Before
bin/rspec ./spec/features/admin/admin_settings_spec.rb:992
# After
WEBDRIVER_HEADLESS=0 bin/rspec ./spec/features/admin/admin_settings_spec.rb:992
```
See [Run `:js` spec in a visible browser](#run-js-spec-in-a-visible-browser) for more info.
##### Search for `Capybara::DSL#` when using profiling
<!-- TODO: Add the search keywords -->
When using [`stackprof` flamegraphs](#profiling-see-where-your-test-spend-its-time), search for `Capybara::DSL#` in the search to see the capybara actions that are made, and how long they take!
#### Identify slow tests
Running a spec with profiling is a good way to start optimizing a spec. This can
@ -437,32 +539,6 @@ performance gains.
When combining tests, consider using `:aggregate_failures`, so that the full
results are available, and not just the first failure.
### General guidelines
- Use a single, top-level `RSpec.describe ClassName` block.
- Use `.method` to describe class methods and `#method` to describe instance
methods.
- Use `context` to test branching logic.
- Try to match the ordering of tests to the ordering in the class.
- Try to follow the [Four-Phase Test](https://thoughtbot.com/blog/four-phase-test) pattern, using newlines
to separate phases.
- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
- Don't assert against the absolute value of a sequence-generated attribute (see
[Gotchas](../gotchas.md#do-not-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
- Avoid using `expect_any_instance_of` or `allow_any_instance_of` (see
[Gotchas](../gotchas.md#avoid-using-expect_any_instance_of-or-allow_any_instance_of-in-rspec)).
- Don't supply the `:each` argument to hooks because it's the default.
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
- When using `evaluate_script("$('.js-foo').testSomething()")` (or `execute_script`) which acts on a given element,
use a Capybara matcher beforehand (such as `find('.js-foo')`) to ensure the element actually exists.
- Use `focus: true` to isolate parts of the specs you want to run.
- Use [`:aggregate_failures`](https://rspec.info/features/3-12/rspec-core/expectation-framework-integration/aggregating-failures/) when there is more than one expectation in a test.
- For [empty test description blocks](https://github.com/rubocop-hq/rspec-style-guide#it-and-specify), use `specify` rather than `it do` if the test is self-explanatory.
- Use `non_existing_record_id`/`non_existing_record_iid`/`non_existing_record_access_level`
when you need an ID/IID/access level that doesn't actually exists. Using 123, 1234,
or even 999 is brittle as these IDs could actually exist in the database in the
context of a CI run.
### Feature category metadata
You must [set feature category metadata for each RSpec example](../feature_categorization/index.md#rspec-examples).

View File

@ -142,7 +142,7 @@ To install a package:
- Connect to the Package Registry for your group:
```shell
composer config repositories.<group_id> composer https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/
composer config repositories.<group_id> composer https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/packages.json
```
- Set the required package version:
@ -159,7 +159,7 @@ To install a package:
"repositories": {
"<group_id>": {
"type": "composer",
"url": "https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/"
"url": "https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/packages.json"
},
...
},
@ -243,7 +243,7 @@ To install a package:
{
...
"repositories": [
{ "type": "composer", "url": "https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/" }
{ "type": "composer", "url": "https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/packages.json" }
],
"config": {
...

View File

@ -69,7 +69,7 @@ If a user is:
### Membership and visibility rights
Depending on their membership type, members of groups or projects are granted different visibility levels
Depending on their membership type, members of groups or projects are granted different [visibility levels](../../../user/public_access.md)
and rights into the group or project.
| Action | Direct group member | Inherited group member | Direct shared group member | Inherited shared group member |

View File

@ -7,18 +7,24 @@ type: reference
# Project and group visibility **(FREE)**
A project in GitLab can be private, internal, or public.
Projects and groups in GitLab can be private, internal, or public.
The visibility level of the group or project has no influence on whether members within the group or project can see each other.
A group or project is an object to allow collaborative work. This is only possible if all members know about each other.
Group or project members can see all members of the group or project they belong to.
Group or project owners can see the origin of membership (the original group or project) of all members.
## Private projects and groups
For private projects, only project members can:
For private projects, only members of the private project or group can:
- Clone the project.
- View the public access directory (`/public`).
Users with the Guest role cannot clone the project.
Private groups can have private subgroups only.
Private groups can have only private subgroups.
## Internal projects and groups **(FREE SELF)**
@ -27,6 +33,8 @@ For internal projects, **any authenticated user**, including users with the Gues
- Clone the project.
- View the public access directory (`/public`).
Only internal members can view internal content.
[External users](admin_area/external_users.md) cannot clone the project.
Internal groups can have internal or private subgroups.

View File

@ -90,15 +90,7 @@ module Gitlab
end
def load_and_validate_expanded_hash!
context.logger.instrument(:config_file_fetch_content_hash) do
content_result # calling the method loads YAML then memoizes the content result
end
context.logger.instrument(:config_file_interpolate_result) do
interpolator.interpolate!
end
return validate_interpolation! unless interpolator.valid?
return errors.push("`#{masked_location}`: #{content_result.error}") unless content_result.valid?
context.logger.instrument(:config_file_expand_content_includes) do
expanded_content_hash # calling the method expands then memoizes the result
@ -109,36 +101,24 @@ module Gitlab
protected
def content_result
::Gitlab::Ci::Config::Yaml
.load_result!(content, project: context.project)
end
strong_memoize_attr :content_result
def content_inputs
# TODO: remove support for `with` syntax in 16.1, see https://gitlab.com/gitlab-org/gitlab/-/issues/408369
# In the interim prefer `inputs` over `with` while allow either syntax.
params.to_h.slice(:inputs, :with).each_value.first
end
strong_memoize_attr :content_inputs
def content_hash
interpolator.interpolate!
interpolator.to_hash
def content_result
context.logger.instrument(:config_file_fetch_content_hash) do
::Gitlab::Ci::Config::Yaml::Loader.new(content, inputs: content_inputs, current_user: context.user).load
end
end
strong_memoize_attr :content_hash
def interpolator
Yaml::Interpolator.new(content_result, content_inputs, context)
end
strong_memoize_attr :interpolator
strong_memoize_attr :content_result
def expanded_content_hash
return if content_hash.blank?
return if content_result.content.blank?
strong_memoize(:expanded_content_hash) do
expand_includes(content_hash)
expand_includes(content_result.content)
end
end
@ -148,12 +128,6 @@ module Gitlab
end
end
def validate_interpolation!
return if interpolator.valid?
errors.push("`#{masked_location}`: #{interpolator.error_message}")
end
def expand_includes(hash)
External::Processor.new(hash, context.mutate(expand_context_attrs)).perform
end

View File

@ -4,21 +4,17 @@ module Gitlab
module Ci
class Config
module Yaml
LoadError = Class.new(StandardError)
class << self
def load!(content, project: nil)
Loader.new(content, project: project).to_result.then do |result|
##
# raise an error for backwards compatibility
#
raise result.error unless result.valid?
def load!(content, current_user: nil)
Loader.new(content, current_user: current_user).load.then do |result|
raise result.error_class, result.error if !result.valid? && result.error_class.present?
raise LoadError, result.error unless result.valid?
result.content
end
end
def load_result!(content, project: nil)
Loader.new(content, project: project).to_result
end
end
end
end

View File

@ -5,42 +5,23 @@ module Gitlab
class Config
module Yaml
##
# Config::Yaml::Interpolation performs includable file interpolation, and surfaces all possible interpolation
# Config::Yaml::Interpolator performs CI config file interpolation, and surfaces all possible interpolation
# errors. It is designed to provide an external file's validation context too.
#
class Interpolator
include ::Gitlab::Utils::StrongMemoize
attr_reader :config, :args, :current_user, :errors
attr_reader :config, :args, :ctx, :errors
def initialize(config, args, ctx = nil)
def initialize(config, args, current_user: nil)
@config = config
@args = args.to_h
@ctx = ctx
@current_user = current_user
@errors = []
validate!
end
def valid?
@errors.none?
end
def ready?
##
# Interpolation is ready when it has been either interrupted by an error or finished with a result.
#
@result || @errors.any?
end
def interpolate?
enabled? && has_header? && valid?
end
def has_header?
config.has_header? && config.header.present?
end
def to_hash
@result.to_h
end
@ -55,43 +36,25 @@ module Gitlab
@errors.first(3).join(', ')
end
##
# TODO Add `instrument.logger` instrumentation blocks:
# https://gitlab.com/gitlab-org/gitlab/-/issues/396722
#
def interpolate!
return {} unless valid?
return @result ||= content.to_h unless interpolate?
return @errors.push(config.error) unless config.valid?
return @result ||= config.content unless config.has_header?
return @errors.concat(header.errors) unless header.valid?
return @errors.concat(inputs.errors) unless inputs.valid?
return @errors.concat(context.errors) unless context.valid?
return @errors.concat(template.errors) unless template.valid?
if ctx&.user
::Gitlab::UsageDataCounters::HLLRedisCounter.track_event('ci_interpolation_users', values: ctx.user.id)
if current_user.present?
::Gitlab::UsageDataCounters::HLLRedisCounter
.track_event('ci_interpolation_users', values: current_user.id)
end
@result ||= template.interpolated.to_h.deep_symbolize_keys
end
strong_memoize_attr :interpolate!
private
def validate!
return errors.push('content does not have a valid YAML syntax') unless config.valid?
return unless has_header? && !enabled?
errors.push('can not evaluate included file because interpolation is disabled')
end
def enabled?
return false if ctx.nil?
::Feature.enabled?(:ci_includable_files_interpolation, ctx.project)
end
def header
@entry ||= Ci::Config::Header::Root.new(config.header).tap do |header|
header.key = 'header'

View File

@ -5,33 +5,45 @@ module Gitlab
class Config
module Yaml
class Loader
include Gitlab::Utils::StrongMemoize
AVAILABLE_TAGS = [Config::Yaml::Tags::Reference].freeze
MAX_DOCUMENTS = 2
def initialize(content, project: nil)
def initialize(content, inputs: {}, current_user: nil)
@content = content
@project = project
@current_user = current_user
@inputs = inputs
end
def to_result
Yaml::Result.new(config: load!, error: nil)
rescue ::Gitlab::Config::Loader::FormatError => e
Yaml::Result.new(error: e)
def load
yaml_result = load_uninterpolated_yaml
return yaml_result unless yaml_result.valid?
interpolator = Yaml::Interpolator.new(yaml_result, inputs, current_user: current_user)
interpolator.interpolate!
if interpolator.valid?
# This Result contains only the interpolated config and does not have a header
Yaml::Result.new(config: interpolator.to_hash, error: nil)
else
Yaml::Result.new(error: interpolator.error_message)
end
end
private
attr_reader :content, :project
attr_reader :content, :current_user, :inputs
def ensure_custom_tags
@ensure_custom_tags ||= begin
AVAILABLE_TAGS.each { |klass| Psych.add_tag(klass.tag, klass) }
true
end
def load_uninterpolated_yaml
Yaml::Result.new(config: load_yaml!, error: nil)
rescue ::Gitlab::Config::Loader::FormatError => e
Yaml::Result.new(error: e.message, error_class: e)
end
def load!
def load_yaml!
ensure_custom_tags
::Gitlab::Config::Loader::MultiDocYaml.new(
@ -41,6 +53,14 @@ module Gitlab
reject_empty: true
).load!
end
def ensure_custom_tags
@ensure_custom_tags ||= begin
AVAILABLE_TAGS.each { |klass| Psych.add_tag(klass.tag, klass) }
true
end
end
end
end
end

View File

@ -5,11 +5,12 @@ module Gitlab
class Config
module Yaml
class Result
attr_reader :error
attr_reader :error, :error_class
def initialize(config: nil, error: nil)
def initialize(config: nil, error: nil, error_class: nil)
@config = Array.wrap(config)
@error = error
@error_class = error_class
end
def valid?

View File

@ -22,6 +22,10 @@ module RuboCop
@offense_count += offense_count
end
def add_files(files)
@files.merge(files)
end
def autocorrectable?
@cop_class&.support_autocorrect?
end

View File

@ -18,6 +18,14 @@ module RuboCop
class TodoFormatter < BaseFormatter
DEFAULT_BASE_DIRECTORY = File.expand_path('../../.rubocop_todo', __dir__)
# Make sure that HAML exclusions are retained.
# This allows enabling cop rules in haml-lint and only exclude HAML files
# with offenses.
#
# See https://gitlab.com/gitlab-org/gitlab/-/issues/415330#caveats
# on why the entry must end with `.html.haml.rb`.
RETAIN_EXCLUSIONS = %r{\.html\.haml\.rb$}
class << self
attr_accessor :base_directory
end
@ -31,7 +39,7 @@ module RuboCop
@config_inspect_todo_dir = load_config_inspect_todo_dir
@config_old_todo_yml = load_config_old_todo_yml
check_multiple_configurations!
create_empty_todos(@config_inspect_todo_dir)
create_todos_retaining_exclusions(@config_inspect_todo_dir)
super
end
@ -81,11 +89,10 @@ module RuboCop
raise "Multiple configurations found for cops:\n#{list}\n"
end
# For each inspected cop TODO config create a TODO object to make sure
# the cop TODO config will be written even without any offenses.
def create_empty_todos(inspected_cop_config)
inspected_cop_config.each_key do |cop_name|
@todos[cop_name]
def create_todos_retaining_exclusions(inspected_cop_config)
inspected_cop_config.each do |cop_name, config|
todo = @todos[cop_name]
todo.add_files(config.fetch('Exclude', []).grep(RETAIN_EXCLUSIONS))
end
end

View File

@ -1,5 +1,6 @@
import { GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@ -31,8 +32,8 @@ describe('RunnerTypeCell', () => {
wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon)
.wrappers[0];
const createComponent = (runner, options) => {
wrapper = mountExtended(RunnerSummaryCell, {
const createComponent = ({ runner, mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(RunnerSummaryCell, {
propsData: {
runner: {
...mockRunner,
@ -40,7 +41,7 @@ describe('RunnerTypeCell', () => {
},
},
stubs: {
RunnerSummaryField,
GlSprintf,
},
...options,
});
@ -51,6 +52,8 @@ describe('RunnerTypeCell', () => {
});
it('Displays the runner name as id and short token', () => {
createComponent({ mountFn: mountExtended });
expect(wrapper.text()).toContain(
`#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`,
);
@ -58,13 +61,16 @@ describe('RunnerTypeCell', () => {
it('Displays no runner manager count', () => {
createComponent({
managers: { count: 0 },
runner: { managers: { nodes: { count: 0 } } },
mountFn: mountExtended,
});
expect(findRunnerManagersBadge().html()).toBe('');
});
it('Displays runner manager count', () => {
createComponent({ mountFn: mountExtended });
expect(findRunnerManagersBadge().text()).toBe('2');
});
@ -74,8 +80,8 @@ describe('RunnerTypeCell', () => {
it('Displays the locked icon for locked runners', () => {
createComponent({
runnerType: PROJECT_TYPE,
locked: true,
runner: { runnerType: PROJECT_TYPE, locked: true },
mountFn: mountExtended,
});
expect(findLockIcon().exists()).toBe(true);
@ -83,8 +89,8 @@ describe('RunnerTypeCell', () => {
it('Displays the runner type', () => {
createComponent({
runnerType: INSTANCE_TYPE,
locked: true,
runner: { runnerType: INSTANCE_TYPE, locked: true },
mountFn: mountExtended,
});
expect(wrapper.text()).toContain(I18N_INSTANCE_TYPE);
@ -101,7 +107,7 @@ describe('RunnerTypeCell', () => {
it('Displays "No description" for missing runner description', () => {
createComponent({
description: null,
runner: { description: null },
});
expect(wrapper.findByText(I18N_NO_DESCRIPTION).classes()).toContain('gl-text-secondary');
@ -109,7 +115,7 @@ describe('RunnerTypeCell', () => {
it('Displays last contact', () => {
createComponent({
contactedAt: '2022-01-02',
runner: { contactedAt: '2022-01-02' },
});
expect(findRunnerSummaryField('clock').findComponent(TimeAgo).props('time')).toBe('2022-01-02');
@ -124,20 +130,46 @@ describe('RunnerTypeCell', () => {
expect(findRunnerSummaryField('clock').text()).toContain(__('Never'));
});
it('Displays ip address', () => {
createComponent({
ipAddress: '127.0.0.1',
describe('IP address', () => {
it('with no managers', () => {
createComponent({
runner: {
managers: { count: 0, nodes: [] },
},
});
expect(findRunnerSummaryField('disk')).toBeUndefined();
});
expect(findRunnerSummaryField('disk').text()).toContain('127.0.0.1');
});
it('with no ip', () => {
createComponent({
runner: {
managers: { count: 1, nodes: [{ ipAddress: null }] },
},
});
it('Displays no ip address', () => {
createComponent({
ipAddress: null,
expect(findRunnerSummaryField('disk')).toBeUndefined();
});
expect(findRunnerSummaryField('disk')).toBeUndefined();
it.each`
count | ipAddress | expected
${1} | ${'127.0.0.1'} | ${'127.0.0.1'}
${2} | ${'127.0.0.2'} | ${'127.0.0.2 (+1)'}
${11} | ${'127.0.0.3'} | ${'127.0.0.3 (+10)'}
${1001} | ${'127.0.0.4'} | ${'127.0.0.4 (+1,000)'}
`(
'with $count managers, ip $ipAddress displays $expected',
({ count, ipAddress, expected }) => {
createComponent({
runner: {
// `first: 1` is requested, `count` varies when there are more managers
managers: { count, nodes: [{ ipAddress }] },
},
});
expect(findRunnerSummaryField('disk').text()).toMatchInterpolatedText(expected);
},
);
});
it('Displays job count', () => {
@ -146,7 +178,7 @@ describe('RunnerTypeCell', () => {
it('Formats large job counts', () => {
createComponent({
jobCount: 1000,
runner: { jobCount: 1000 },
});
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000');
@ -154,7 +186,7 @@ describe('RunnerTypeCell', () => {
it('Formats large job counts with a plus symbol', () => {
createComponent({
jobCount: 1001,
runner: { jobCount: 1001 },
});
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+');
@ -165,7 +197,7 @@ describe('RunnerTypeCell', () => {
it('Displays created at ...', () => {
createComponent({
createdBy: null,
runner: { createdBy: null },
});
expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
@ -177,12 +209,15 @@ describe('RunnerTypeCell', () => {
});
it('Displays created at ... by ...', () => {
createComponent({ mountFn: mountExtended });
expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
sprintf(I18N_CREATED_AT_BY_LABEL, {
timeAgo: findCreatedTime().text(),
avatar: mockRunner.createdBy.username,
}),
);
expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
});
@ -200,7 +235,7 @@ describe('RunnerTypeCell', () => {
it('Displays tag list', () => {
createComponent({
tagList: ['shell', 'linux'],
runner: { tagList: ['shell', 'linux'] },
});
expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']);
@ -209,14 +244,11 @@ describe('RunnerTypeCell', () => {
it('Displays a custom runner-name slot', () => {
const slotContent = 'My custom runner name';
createComponent(
{},
{
slots: {
'runner-name': slotContent,
},
createComponent({
slots: {
'runner-name': slotContent,
},
);
});
expect(wrapper.text()).toContain(slotContent);
});

View File

@ -106,7 +106,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
it 'is not a valid file' do
expect(valid?).to be_falsy
expect(file.error_message)
.to eq('`some/file/xxxxxxxxxxxxxxxx.yml`: content does not have a valid YAML syntax')
.to eq('`some/file/xxxxxxxxxxxxxxxx.yml`: Invalid configuration format')
end
end
@ -128,31 +128,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
end
end
context 'when interpolation is disabled but there is a spec header' do
before do
stub_feature_flags(ci_includable_files_interpolation: false)
end
let(:location) { 'some-location.yml' }
let(:content) do
<<~YAML
spec:
include:
website:
---
run:
script: deploy $[[ inputs.website ]]
YAML
end
it 'returns an error saying that interpolation is disabled' do
expect(valid?).to be_falsy
expect(file.errors)
.to include('`some-location.yml`: can not evaluate included file because interpolation is disabled')
end
end
context 'when interpolation was unsuccessful' do
let(:location) { 'some-location.yml' }
@ -275,4 +250,17 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
it { is_expected.to eq([{ location: location, content: content }, nil, 'HEAD'].hash) }
end
end
describe '#load_and_validate_expanded_hash!' do
let(:location) { 'some/file/config.yml' }
let(:logger) { instance_double(::Gitlab::Ci::Pipeline::Logger, :instrument) }
let(:context_params) { { sha: 'HEAD', variables: variables, project: project, logger: logger } }
it 'includes instrumentation for loading and expanding the content' do
expect(logger).to receive(:instrument).once.ordered.with(:config_file_fetch_content_hash).and_yield
expect(logger).to receive(:instrument).once.ordered.with(:config_file_expand_content_includes).and_yield
file.load_and_validate_expanded_hash!
end
end
end

View File

@ -121,7 +121,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category:
it 'is invalid' do
expect(subject).to be_falsy
expect(external_resource.error_message).to match(/does not have a valid YAML syntax/)
expect(external_resource.error_message).to match(/Invalid configuration format/)
end
end
end

View File

@ -221,7 +221,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
it 'raises an error' do
expect { processor.perform }.to raise_error(
described_class::IncludeError,
'`lib/gitlab/ci/templates/template.yml`: content does not have a valid YAML syntax'
'`lib/gitlab/ci/templates/template.yml`: Invalid configuration format'
)
end
end

View File

@ -5,10 +5,10 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeline_composition do
let_it_be(:project) { create(:project) }
let(:ctx) { instance_double(Gitlab::Ci::Config::External::Context, project: project, user: build(:user, id: 1234)) }
let(:current_user) { build(:user, id: 1234) }
let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(config: [header, content]) }
subject { described_class.new(result, arguments, ctx) }
subject { described_class.new(result, arguments, current_user: current_user) }
context 'when input data is valid' do
let(:header) do
@ -39,7 +39,7 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
end
context 'when config has a syntax error' do
let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new) }
let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: 'Invalid configuration format') }
let(:arguments) do
{ website: 'gitlab.com' }
@ -50,7 +50,7 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
expect(subject).not_to be_valid
expect(subject.error_message).to eq subject.errors.first
expect(subject.errors).to include 'content does not have a valid YAML syntax'
expect(subject.errors).to include 'Invalid configuration format'
end
end
@ -142,28 +142,6 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
end
describe '#to_hash' do
context 'when interpolation is disabled' do
before do
stub_feature_flags(ci_includable_files_interpolation: false)
end
let(:header) do
{ spec: { inputs: { website: nil } } }
end
let(:content) do
{ test: 'deploy $[[ inputs.website ]]' }
end
let(:arguments) { {} }
it 'returns an empty hash' do
subject.interpolate!
expect(subject.to_hash).to be_empty
end
end
context 'when interpolation is not used' do
let(:result) do
::Gitlab::Ci::Config::Yaml::Result.new(config: content)
@ -202,118 +180,4 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
end
end
end
describe '#ready?' do
let(:header) do
{ spec: { inputs: { website: nil } } }
end
let(:content) do
{ test: 'deploy $[[ inputs.website ]]' }
end
let(:arguments) do
{ website: 'gitlab.com' }
end
it 'returns false if interpolation has not been done yet' do
expect(subject).not_to be_ready
end
it 'returns true if interpolation has been performed' do
subject.interpolate!
expect(subject).to be_ready
end
context 'when interpolation can not be performed' do
let(:result) do
::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new)
end
it 'returns true if interpolator has preliminary errors' do
expect(subject).to be_ready
end
it 'returns true if interpolation has been attempted' do
subject.interpolate!
expect(subject).to be_ready
end
end
end
describe '#interpolate?' do
let(:header) do
{ spec: { inputs: { website: nil } } }
end
let(:content) do
{ test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' }
end
let(:arguments) do
{ website: 'gitlab.com' }
end
context 'when interpolation can be performed' do
it 'will perform interpolation' do
expect(subject.interpolate?).to eq true
end
end
context 'when interpolation is disabled' do
before do
stub_feature_flags(ci_includable_files_interpolation: false)
end
it 'will not perform interpolation' do
expect(subject.interpolate?).to eq false
end
end
context 'when an interpolation header is missing' do
let(:header) { nil }
it 'will not perform interpolation' do
expect(subject.interpolate?).to eq false
end
end
context 'when interpolator has preliminary errors' do
let(:result) do
::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new)
end
it 'will not perform interpolation' do
expect(subject.interpolate?).to eq false
end
end
end
describe '#has_header?' do
let(:content) do
{ test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' }
end
let(:arguments) do
{ website: 'gitlab.com' }
end
context 'when header is an empty hash' do
let(:header) { {} }
it 'does not have a header available' do
expect(subject).not_to have_header
end
end
context 'when header is not specified' do
let(:header) { nil }
it 'does not have a header available' do
expect(subject).not_to have_header
end
end
end
end

View File

@ -2,151 +2,58 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_composition do
describe '#to_result' do
RSpec.describe ::Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_composition do
describe '#load' do
let_it_be(:project) { create(:project) }
subject(:result) { described_class.new(yaml, project: project).to_result }
let(:inputs) { { test_input: 'hello test' } }
context 'when syntax is invalid' do
let(:yaml) { 'some: invalid: syntax' }
let(:yaml) do
<<~YAML
---
spec:
inputs:
test_input:
---
test_job:
script:
- echo "$[[ inputs.test_input ]]"
YAML
end
it 'returns an invalid result object' do
subject(:result) { described_class.new(yaml, inputs: inputs, current_user: project.creator).load }
it 'loads and interpolates CI config YAML' do
expected_config = { test_job: { script: ['echo "hello test"'] } }
expect(result).to be_valid
expect(result.content).to eq(expected_config)
end
it 'allows the use of YAML reference tags' do
expect(Psych).to receive(:add_tag).once.with(
::Gitlab::Ci::Config::Yaml::Tags::Reference.tag,
::Gitlab::Ci::Config::Yaml::Tags::Reference
)
result
end
context 'when there is an error loading the YAML' do
let(:yaml) { 'invalid...yaml' }
it 'returns an error result' do
expect(result).not_to be_valid
expect(result.error).to be_a ::Gitlab::Config::Loader::FormatError
expect(result.error).to eq('Invalid configuration format')
end
end
context 'when the first document is a header' do
context 'with explicit document start marker' do
let(:yaml) do
<<~YAML
---
spec:
---
b: 2
YAML
end
context 'when there is an error interpolating the YAML' do
let(:inputs) { {} }
it 'considers the first document as header and the second as content' do
expect(result).to be_valid
expect(result.error).to be_nil
expect(result.header).to eq({ spec: nil })
expect(result.content).to eq({ b: 2 })
end
end
end
context 'when first document is empty' do
let(:yaml) do
<<~YAML
---
---
b: 2
YAML
end
it 'considers the first document as header and the second as content' do
expect(result).not_to have_header
end
end
context 'when first document is an empty hash' do
let(:yaml) do
<<~YAML
{}
---
b: 2
YAML
end
it 'returns second document as a content' do
expect(result).not_to have_header
expect(result.content).to eq({ b: 2 })
end
end
context 'when first an array' do
let(:yaml) do
<<~YAML
---
- a
- b
---
b: 2
YAML
end
it 'considers the first document as header and the second as content' do
expect(result).not_to have_header
end
end
context 'when the first document is not a header' do
let(:yaml) do
<<~YAML
a: 1
---
b: 2
YAML
end
it 'considers the first document as content for backwards compatibility' do
expect(result).to be_valid
expect(result.error).to be_nil
expect(result).not_to have_header
expect(result.content).to eq({ a: 1 })
end
context 'with explicit document start marker' do
let(:yaml) do
<<~YAML
---
a: 1
---
b: 2
YAML
end
it 'considers the first document as content for backwards compatibility' do
expect(result).to be_valid
expect(result.error).to be_nil
expect(result).not_to have_header
expect(result.content).to eq({ a: 1 })
end
end
end
context 'when the first document is not a header and second document is empty' do
let(:yaml) do
<<~YAML
a: 1
---
YAML
end
it 'considers the first document as content' do
expect(result).to be_valid
expect(result.error).to be_nil
expect(result).not_to have_header
expect(result.content).to eq({ a: 1 })
end
context 'with explicit document start marker' do
let(:yaml) do
<<~YAML
---
a: 1
---
YAML
end
it 'considers the first document as content' do
expect(result).to be_valid
expect(result.error).to be_nil
expect(result).not_to have_header
expect(result.content).to eq({ a: 1 })
end
it 'returns an error result' do
expect(result).not_to be_valid
expect(result.error).to eq('`test_input` input: required value has not been provided')
end
end
end

View File

@ -3,18 +3,20 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition do
let(:yaml) do
<<~YAML
image: 'image:1.0'
texts:
nested_key: 'value1'
more_text:
more_nested_key: 'value2'
YAML
end
describe '.load!' do
subject(:config) { described_class.load!(yaml) }
it 'loads a YAML file' do
yaml = <<~YAML
image: 'image:1.0'
texts:
nested_key: 'value1'
more_text:
more_nested_key: 'value2'
YAML
config = described_class.load!(yaml)
expect(config).to eq({
image: 'image:1.0',
texts: {
@ -30,156 +32,20 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition
let(:yaml) { 'some: invalid: syntax' }
it 'raises an error' do
expect { described_class.load!(yaml) }
expect { config }
.to raise_error ::Gitlab::Config::Loader::FormatError, /mapping values are not allowed in this context/
end
end
end
describe '.load_result!' do
let_it_be(:project) { create(:project) }
context 'when given a user' do
let(:user) { instance_double(User) }
subject(:result) { described_class.load_result!(yaml, project: project) }
subject(:config) { described_class.load!(yaml, current_user: user) }
context 'when syntax is invalid' do
let(:yaml) { 'some: invalid: syntax' }
it 'passes it to Loader' do
expect(::Gitlab::Ci::Config::Yaml::Loader).to receive(:new).with(yaml, current_user: user).and_call_original
it 'returns an invalid result object' do
expect(result).not_to be_valid
expect(result.error).to be_a ::Gitlab::Config::Loader::FormatError
end
end
context 'when the first document is a header' do
context 'with explicit document start marker' do
let(:yaml) do
<<~YAML
---
spec:
---
b: 2
YAML
end
it 'considers the first document as header and the second as content' do
expect(result).to be_valid
expect(result.error).to be_nil
expect(result.header).to eq({ spec: nil })
expect(result.content).to eq({ b: 2 })
end
end
end
context 'when first document is empty' do
let(:yaml) do
<<~YAML
---
---
b: 2
YAML
end
it 'considers the first document as header and the second as content' do
expect(result).not_to have_header
end
end
context 'when first document is an empty hash' do
let(:yaml) do
<<~YAML
{}
---
b: 2
YAML
end
it 'returns second document as a content' do
expect(result).not_to have_header
expect(result.content).to eq({ b: 2 })
end
end
context 'when first an array' do
let(:yaml) do
<<~YAML
---
- a
- b
---
b: 2
YAML
end
it 'considers the first document as header and the second as content' do
expect(result).not_to have_header
end
end
context 'when the first document is not a header' do
let(:yaml) do
<<~YAML
a: 1
---
b: 2
YAML
end
it 'considers the first document as content for backwards compatibility' do
expect(result).to be_valid
expect(result.error).to be_nil
expect(result).not_to have_header
expect(result.content).to eq({ a: 1 })
end
context 'with explicit document start marker' do
let(:yaml) do
<<~YAML
---
a: 1
---
b: 2
YAML
end
it 'considers the first document as content for backwards compatibility' do
expect(result).to be_valid
expect(result.error).to be_nil
expect(result).not_to have_header
expect(result.content).to eq({ a: 1 })
end
end
end
context 'when the first document is not a header and second document is empty' do
let(:yaml) do
<<~YAML
a: 1
---
YAML
end
it 'considers the first document as content' do
expect(result).to be_valid
expect(result.error).to be_nil
expect(result).not_to have_header
expect(result.content).to eq({ a: 1 })
end
context 'with explicit document start marker' do
let(:yaml) do
<<~YAML
---
a: 1
---
YAML
end
it 'considers the first document as content' do
expect(result).to be_valid
expect(result.error).to be_nil
expect(result).not_to have_header
expect(result.content).to eq({ a: 1 })
end
config
end
end
end

View File

@ -364,32 +364,4 @@ RSpec.describe PlanLimits do
end
end
end
describe '#limit_attribute_changes', :freeze_time do
let(:user) { create(:user) }
let(:current_timestamp) { Time.current.utc.to_i }
let(:plan_limits) do
create(:plan_limits,
limits_history: { 'enforcement_limit' => [
{ user_id: user.id, username: user.username, timestamp: current_timestamp,
value: 20_000 }, { user_id: user.id, username: user.username, timestamp: current_timestamp,
value: 50_000 }
] })
end
it 'returns an empty array for attribute with no changes' do
changes = plan_limits.limit_attribute_changes(:notification_limit)
expect(changes).to eq([])
end
it 'returns the changes for a specific attribute' do
changes = plan_limits.limit_attribute_changes(:enforcement_limit)
expect(changes).to eq(
[{ timestamp: current_timestamp, value: 20_000, username: user.username, user_id: user.id },
{ timestamp: current_timestamp, value: 50_000, username: user.username, user_id: user.id }]
)
end
end
end

View File

@ -6576,7 +6576,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it 'does not allow access to branches for which the merge request was closed' do
create(
:merge_request, :closed,
:merge_request,
:closed,
target_project: target_project,
target_branch: 'target-branch',
source_project: project,
@ -9081,7 +9082,9 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
def create_build(new_pipeline = pipeline, name = 'test')
create(
:ci_build, :success, :artifacts,
:ci_build,
:success,
:artifacts,
pipeline: new_pipeline,
status: new_pipeline.status,
name: name

View File

@ -3,7 +3,7 @@
require 'rubocop_spec_helper'
require_relative '../../rubocop/cop_todo'
RSpec.describe RuboCop::CopTodo do
RSpec.describe RuboCop::CopTodo, feature_category: :tooling do
let(:cop_name) { 'Cop/Rule' }
subject(:cop_todo) { described_class.new(cop_name) }
@ -32,6 +32,19 @@ RSpec.describe RuboCop::CopTodo do
end
end
describe '#add_files' do
it 'adds files' do
cop_todo.add_files(%w[a.rb b.rb])
cop_todo.add_files(%w[a.rb])
cop_todo.add_files(%w[])
expect(cop_todo).to have_attributes(
files: contain_exactly('a.rb', 'b.rb'),
offense_count: 0
)
end
end
describe '#autocorrectable?' do
subject { cop_todo.autocorrectable? }

View File

@ -1,4 +1,5 @@
# frozen_string_literal: true
# rubocop:disable RSpec/VerifiedDoubles
require 'fast_spec_helper'
@ -10,7 +11,7 @@ require 'tmpdir'
require_relative '../../../rubocop/formatter/todo_formatter'
require_relative '../../../rubocop/todo_dir'
RSpec.describe RuboCop::Formatter::TodoFormatter do
RSpec.describe RuboCop::Formatter::TodoFormatter, feature_category: :tooling do
let(:stdout) { StringIO.new }
let(:tmp_dir) { Dir.mktmpdir }
let(:real_tmp_dir) { File.join(tmp_dir, 'real') }
@ -97,6 +98,36 @@ RSpec.describe RuboCop::Formatter::TodoFormatter do
YAML
end
context 'with existing HAML exclusions' do
before do
todo_dir.write('B/TooManyOffenses', <<~YAML)
---
B/TooManyOffenses:
Exclude:
- 'd.rb'
- 'app/views/project.html.haml.rb'
- 'app/views/unrelated.html.haml.rb.ext'
- 'app/views/unrelated.html.haml.ext'
- 'app/views/unrelated.html.haml'
YAML
todo_dir.inspect_all
end
it 'does not remove them' do
run_formatter
expect(todo_yml('B/TooManyOffenses')).to eq(<<~YAML)
---
B/TooManyOffenses:
Exclude:
- 'a.rb'
- 'app/views/project.html.haml.rb'
- 'c.rb'
YAML
end
end
context 'when cop previously not explicitly disabled' do
before do
todo_dir.write('B/TooManyOffenses', <<~YAML)
@ -105,6 +136,8 @@ RSpec.describe RuboCop::Formatter::TodoFormatter do
Exclude:
- 'x.rb'
YAML
todo_dir.inspect_all
end
it 'does not disable cop' do
@ -158,6 +191,8 @@ RSpec.describe RuboCop::Formatter::TodoFormatter do
Exclude:
- 'x.rb'
YAML
todo_dir.inspect_all
end
it 'keeps cop disabled' do

View File

@ -2035,7 +2035,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
expect(pipeline).to be_persisted
expect(pipeline.yaml_errors)
.to include 'content does not have a valid YAML syntax'
.to include 'mapping values are not allowed'
end
end
end
@ -2172,7 +2172,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
expect(pipeline).to be_persisted
expect(pipeline.yaml_errors)
.to include 'content does not have a valid YAML syntax'
.to include 'mapping values are not allowed'
end
end
end