Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									11501d600a
								
							
						
					
					
						commit
						b28ebf3730
					
				| 
						 | 
				
			
			@ -15,9 +15,9 @@ module Import
 | 
			
		|||
 | 
			
		||||
      if result.success?
 | 
			
		||||
        flash[:raw] = banner('accept_invite')
 | 
			
		||||
        redirect_to(dashboard_groups_path)
 | 
			
		||||
        redirect_to(root_path)
 | 
			
		||||
      else
 | 
			
		||||
        redirect_to(dashboard_groups_path, alert: s_('UserMapping|The invitation could not be accepted.'))
 | 
			
		||||
        redirect_to(root_path, alert: s_('UserMapping|The invitation could not be accepted.'))
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -26,9 +26,9 @@ module Import
 | 
			
		|||
 | 
			
		||||
      if result.success?
 | 
			
		||||
        flash[:raw] = banner('reject_invite')
 | 
			
		||||
        redirect_to(dashboard_groups_path)
 | 
			
		||||
        redirect_to(root_path)
 | 
			
		||||
      else
 | 
			
		||||
        redirect_to(dashboard_groups_path, alert: s_('UserMapping|The invitation could not be declined.'))
 | 
			
		||||
        redirect_to(root_path, alert: s_('UserMapping|The invitation could not be declined.'))
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +40,7 @@ module Import
 | 
			
		|||
      return if source_user.awaiting_approval? && current_user_matches_invite?
 | 
			
		||||
 | 
			
		||||
      flash[:raw] = banner('invalid_invite')
 | 
			
		||||
      redirect_to(dashboard_groups_path)
 | 
			
		||||
      redirect_to(root_path)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def current_user_matches_invite?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -584,10 +584,6 @@ vulnerability_scanners:
 | 
			
		|||
  - table: projects
 | 
			
		||||
    column: project_id
 | 
			
		||||
    on_delete: async_delete
 | 
			
		||||
vulnerability_state_transitions:
 | 
			
		||||
  - table: ci_pipelines
 | 
			
		||||
    column: state_changed_at_pipeline_id
 | 
			
		||||
    on_delete: async_nullify
 | 
			
		||||
vulnerability_statistics:
 | 
			
		||||
  - table: ci_pipelines
 | 
			
		||||
    column: latest_pipeline_id
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
# Ensures content nested in lists are spaced correctly.
 | 
			
		||||
#
 | 
			
		||||
extends: existence
 | 
			
		||||
message: "Use three spaces for lines under ordered lists, and two spaces under unordered lists"
 | 
			
		||||
message: "Items under an ordered list must be indented three spaces. Items under an unordered list must be indented two spaces."
 | 
			
		||||
link: https://docs.gitlab.com/ee/development/documentation/styleguide/#nesting-inside-a-list-item
 | 
			
		||||
level: error
 | 
			
		||||
nonword: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,6 +28,7 @@ For more information on:
 | 
			
		|||
 | 
			
		||||
## Hashed storage
 | 
			
		||||
 | 
			
		||||
> - Support for legacy storage, where repository paths were generated based on the project path, has been completely removed in GitLab 14.0.
 | 
			
		||||
> - **Storage name** field [renamed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128416) from **Gitaly storage name** and **Relative path** field [renamed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128416) from **Gitaly relative path** in GitLab 16.3.
 | 
			
		||||
 | 
			
		||||
Hashed storage stores projects on disk in a location based on a hash of the project's ID. This makes the folder
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -163,6 +163,21 @@ Example of response
 | 
			
		|||
      "runner": null,
 | 
			
		||||
      "artifacts_expire_at": null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "cluster_agent": {
 | 
			
		||||
    "id": 1,
 | 
			
		||||
    "name": "agent-1",
 | 
			
		||||
    "config_project": {
 | 
			
		||||
      "id": 20,
 | 
			
		||||
      "description": "",
 | 
			
		||||
      "name": "test",
 | 
			
		||||
      "name_with_namespace": "Administrator / test",
 | 
			
		||||
      "path": "test",
 | 
			
		||||
      "path_with_namespace": "root/test",
 | 
			
		||||
      "created_at": "2022-03-20T20:42:40.221Z"
 | 
			
		||||
    },
 | 
			
		||||
    "created_at": "2022-04-20T20:42:40.221Z",
 | 
			
		||||
    "created_by_user_id": 42
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,7 +45,7 @@ they are still not 100% standardized. You can see them below:
 | 
			
		|||
| User snippet attachments              | yes    | `uploads/-/system/personal_snippet/:id/:random_hex/:filename` | `PersonalFileUploader` | Snippet    |
 | 
			
		||||
| Project avatars                       | yes    | `uploads/-/system/project/avatar/:id/:filename`               | `AvatarUploader`       | Project    |
 | 
			
		||||
| Topic avatars                         | yes    | `uploads/-/system/projects/topic/avatar/:id/:filename`        | `AvatarUploader`       | Topic      |
 | 
			
		||||
| Issues/MR/Notes Markdown attachments  | yes    | `uploads/:project_path_with_namespace/:random_hex/:filename`  | `FileUploader`         | Project    |
 | 
			
		||||
| Issues/MR/Notes Markdown attachments  | yes    | `uploads/:hash_project_id/:random_hex/:filename`  | `FileUploader`         | Project    |
 | 
			
		||||
| Issues/MR/Notes Legacy Markdown attachments | no | `uploads/-/system/note/attachment/:id/:filename`            | `AttachmentUploader`   | Note       |
 | 
			
		||||
| Design Management design thumbnails   | yes | `uploads/-/system/design_management/action/image_v432x230/:id/:filename` | `DesignManagement::DesignV432x230Uploader` | DesignManagement::Action |
 | 
			
		||||
| CI Artifacts (CE)                     | yes    | `shared/artifacts/:disk_hash[0..1]/:disk_hash[2..3]/:disk_hash/:year_:month_:date/:job_id/:job_artifact_id` (`:disk_hash` is SHA256 digest of `project_id`) | `JobArtifactUploader`  | Ci::JobArtifact  |
 | 
			
		||||
| 
						 | 
				
			
			@ -56,9 +56,8 @@ they are still not 100% standardized. You can see them below:
 | 
			
		|||
CI Artifacts and LFS Objects behave differently in CE and EE. In CE they inherit the `GitlabUploader`
 | 
			
		||||
while in EE they inherit the `ObjectStorage` and store files in and S3 API compatible object store.
 | 
			
		||||
 | 
			
		||||
In the case of Issues/MR/Notes Markdown attachments, there is a different approach using the [Hashed Storage](../administration/repository_storage_paths.md) layout,
 | 
			
		||||
instead of basing the path into a mutable variable `:project_path_with_namespace`, it's possible to use the
 | 
			
		||||
hash of the project ID instead, if project migrates to the new approach (introduced in 10.2).
 | 
			
		||||
Attachments for issues, merge requests (MR), and notes in Markdown use
 | 
			
		||||
[hashed storage](../administration/repository_storage_paths.md) with the hash of the project ID.
 | 
			
		||||
 | 
			
		||||
We provide an [all-in-one Rake task](../administration/raketasks/uploads/migrate.md)
 | 
			
		||||
to migrate all uploads to object storage in one go. If a new Uploader class or model
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -98,9 +98,6 @@ are as follows:
 | 
			
		|||
 | 
			
		||||
### Assumptions
 | 
			
		||||
 | 
			
		||||
- All repositories in a pool must use [hashed storage](../administration/repository_storage_paths.md).
 | 
			
		||||
  This is so that we don't have to ever worry about updating paths in
 | 
			
		||||
  `object/info/alternates` files.
 | 
			
		||||
- All repositories in a pool must be on the same Gitaly storage shard.
 | 
			
		||||
  The Git alternates mechanism relies on direct disk access across
 | 
			
		||||
  multiple repositories, and we can only assume direct disk access to
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -192,6 +192,14 @@ The strategy takes precedence over other policies using the `inject_ci` strategy
 | 
			
		|||
 | 
			
		||||
This strategy allows users to include the project CI/CD configuration in the pipeline execution policy configuration, enabling them to customize the policy jobs. For example, by combining policy and project CI/CD configuration into one YAML file, users can override `before_script` configuration.
 | 
			
		||||
 | 
			
		||||
NOTE:
 | 
			
		||||
When a pipeline execution policy uses workflow rules that prevent policy jobs from running, the
 | 
			
		||||
project's original CI/CD configuration remains in effect instead of being overridden. You can
 | 
			
		||||
conditionally apply pipeline execution policies to control when the policy impacts the project's
 | 
			
		||||
CI/CD configuration. For example, if you set a workflow rule `if: $CI_PIPELINE_SOURCE ==
 | 
			
		||||
"merge_request_event"`, the project's CI configuration is only overridden when the pipeline source
 | 
			
		||||
is a merge request event.
 | 
			
		||||
 | 
			
		||||
### Include a project's CI/CD configuration in the pipeline execution policy configuration
 | 
			
		||||
 | 
			
		||||
When using `override_project_ci` strategy, the project configuration can be included into the pipeline execution policy configuration:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,8 +36,6 @@ For a video overview, see [Design Management](https://www.youtube.com/watch?v=CC
 | 
			
		|||
  Image thumbnails are stored as other uploads, and are not associated with a project but rather
 | 
			
		||||
  with a specific design model.
 | 
			
		||||
 | 
			
		||||
  Newly created projects use hashed storage by default.
 | 
			
		||||
 | 
			
		||||
  A GitLab administrator can verify the relative path of a hashed-stored project by going to **Admin area > Projects**
 | 
			
		||||
  and then selecting the project in question. The **Relative path** field contains `@hashed` in its value.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,15 @@ module API
 | 
			
		|||
      expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true }
 | 
			
		||||
      expose :state, documentation: { type: 'string', example: 'available' }
 | 
			
		||||
      expose :auto_stop_at, documentation: { type: 'dateTime', example: '2019-05-25T18:55:13.252Z' }
 | 
			
		||||
      expose :cluster_agent, using: Entities::Clusters::Agent, if: ->(_, _) { can_read_cluster_agent? }
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def can_read_cluster_agent?
 | 
			
		||||
        return unless object.cluster_agent.present?
 | 
			
		||||
 | 
			
		||||
        Ability.allowed?(options[:current_user], :read_cluster_agent, object.cluster_agent)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,8 +3,13 @@
 | 
			
		|||
module Import
 | 
			
		||||
  module PlaceholderReferences
 | 
			
		||||
    module AliasResolver
 | 
			
		||||
      extend self
 | 
			
		||||
 | 
			
		||||
      MissingAlias = Class.new(StandardError)
 | 
			
		||||
 | 
			
		||||
      NOTE_COLUMNS = { "author_id" => "author_id", "updated_by_id" => "updated_by_id",
 | 
			
		||||
                       "resolved_by_id" => "resolved_by_id" }.freeze
 | 
			
		||||
 | 
			
		||||
      # A new version for a model should be defined when new entries must be
 | 
			
		||||
      # mapped in a different way to data that already exists in the database.
 | 
			
		||||
      # Context: https://gitlab.com/gitlab-org/gitlab/-/issues/478501
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +35,7 @@ module Import
 | 
			
		|||
        "Ci::Build" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: Ci::Build,
 | 
			
		||||
            columns: { "user_id" => "user_id" }
 | 
			
		||||
            columns: { "user_id" => "user_id", "erased_by_id" => "erased_by_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "Ci::Pipeline" => {
 | 
			
		||||
| 
						 | 
				
			
			@ -39,28 +44,60 @@ module Import
 | 
			
		|||
            columns: { "user_id" => "user_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "Ci::PipelineSchedule" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: Ci::PipelineSchedule,
 | 
			
		||||
            columns: { "owner_id" => "owner_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "DesignManagement::Version" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: DesignManagement::Version,
 | 
			
		||||
            columns: { "author_id" => "author_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "DiffNote" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: DiffNote,
 | 
			
		||||
            columns: NOTE_COLUMNS
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "DiscussionNote" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: DiscussionNote,
 | 
			
		||||
            columns: NOTE_COLUMNS
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "Event" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: Event,
 | 
			
		||||
            columns: { "author_id" => "author_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "Epic" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: Epic,
 | 
			
		||||
            columns: { "author_id" => "author_id", "assignee_id" => "assignee_id", "updated_by_id" => "updated_by_id",
 | 
			
		||||
                       "last_edited_by_id" => "last_edited_by_id", "closed_by_id" => "closed_by_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "GenericCommitStatus" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: GenericCommitStatus,
 | 
			
		||||
            columns: { "user_id" => "user_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "LegacyDiffNote" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: LegacyDiffNote,
 | 
			
		||||
            columns: NOTE_COLUMNS
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "Issue" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: Issue,
 | 
			
		||||
            columns: { "author_id" => "author_id" }
 | 
			
		||||
            columns: { "author_id" => "author_id", "updated_by_id" => "updated_by_id",
 | 
			
		||||
                       "closed_by_id" => "closed_by_id", "last_edited_by_id" => "last_edited_by_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "IssueAssignee" => {
 | 
			
		||||
| 
						 | 
				
			
			@ -69,16 +106,23 @@ module Import
 | 
			
		|||
            columns: { "user_id" => "user_id", "issue_id" => "issue_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "List" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: List,
 | 
			
		||||
            columns: { "user_id" => "user_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "MergeRequest" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: MergeRequest,
 | 
			
		||||
            columns: { "author_id" => "author_id" }
 | 
			
		||||
            columns: { "author_id" => "author_id", "updated_by_id" => "updated_by_id",
 | 
			
		||||
                       "last_edited_by_id" => "last_edited_by_id", "merge_user_id" => "merge_user_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "MergeRequest::Metrics" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: MergeRequest::Metrics,
 | 
			
		||||
            columns: { "merged_by_id" => "merged_by_id" }
 | 
			
		||||
            columns: { "merged_by_id" => "merged_by_id", "latest_closed_by_id" => "latest_closed_by_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "MergeRequestAssignee" => {
 | 
			
		||||
| 
						 | 
				
			
			@ -96,6 +140,36 @@ module Import
 | 
			
		|||
        "Note" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: Note,
 | 
			
		||||
            columns: NOTE_COLUMNS
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "ProtectedTag::CreateAccessLevel" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: ProtectedTag::CreateAccessLevel,
 | 
			
		||||
            columns: { "user_id" => "user_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "ProtectedBranch::MergeAccessLevel" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: ProtectedBranch::MergeAccessLevel,
 | 
			
		||||
            columns: { "user_id" => "user_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "ProtectedBranch::PushAccessLevel" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: ProtectedBranch::PushAccessLevel,
 | 
			
		||||
            columns: { "user_id" => "user_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "ProjectSnippet" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: ProjectSnippet,
 | 
			
		||||
            columns: { "author_id" => "author_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "Release" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: Release,
 | 
			
		||||
            columns: { "author_id" => "author_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			@ -117,6 +191,12 @@ module Import
 | 
			
		|||
            columns: { "user_id" => "user_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "Snippet" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: Snippet,
 | 
			
		||||
            columns: { "author_id" => "author_id" }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "Timelog" => {
 | 
			
		||||
          1 => {
 | 
			
		||||
            model: Timelog,
 | 
			
		||||
| 
						 | 
				
			
			@ -125,16 +205,22 @@ module Import
 | 
			
		|||
        }
 | 
			
		||||
      }.freeze
 | 
			
		||||
 | 
			
		||||
      def self.version_for_model(model)
 | 
			
		||||
        return ALIASES[model].keys.max if ALIASES[model]
 | 
			
		||||
      private_constant :ALIASES
 | 
			
		||||
 | 
			
		||||
      def aliases
 | 
			
		||||
        ALIASES
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def version_for_model(model)
 | 
			
		||||
        return aliases[model].keys.max if aliases[model]
 | 
			
		||||
 | 
			
		||||
        track_error_for_missing(model: model)
 | 
			
		||||
 | 
			
		||||
        1
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def self.aliased_model(model, version:)
 | 
			
		||||
        aliased_model = ALIASES.dig(model, version, :model)
 | 
			
		||||
      def aliased_model(model, version:)
 | 
			
		||||
        aliased_model = aliases.dig(model, version, :model)
 | 
			
		||||
        return aliased_model if aliased_model.present?
 | 
			
		||||
 | 
			
		||||
        track_error_for_missing(model: model, version: version)
 | 
			
		||||
| 
						 | 
				
			
			@ -142,8 +228,8 @@ module Import
 | 
			
		|||
        model.safe_constantize
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def self.aliased_column(model, column, version:)
 | 
			
		||||
        aliased_column = ALIASES.dig(model, version, :columns, column)
 | 
			
		||||
      def aliased_column(model, column, version:)
 | 
			
		||||
        aliased_column = aliases.dig(model, version, :columns, column)
 | 
			
		||||
        return aliased_column if aliased_column.present?
 | 
			
		||||
 | 
			
		||||
        track_error_for_missing(model: model, column: column, version: version)
 | 
			
		||||
| 
						 | 
				
			
			@ -151,8 +237,8 @@ module Import
 | 
			
		|||
        column
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def self.models_with_columns
 | 
			
		||||
        ALIASES.values
 | 
			
		||||
      def models_with_columns
 | 
			
		||||
        aliases.values
 | 
			
		||||
          .map { |versions| versions[versions.keys.max] }
 | 
			
		||||
          .map { |data| [data[:model], data[:columns].values] }
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			@ -166,3 +252,5 @@ module Import
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
Import::PlaceholderReferences::AliasResolver.extend_mod
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -179,6 +179,7 @@ RSpec.describe 'Database schema', feature_category: :database do
 | 
			
		|||
    vulnerability_identifiers: %w[external_id],
 | 
			
		||||
    vulnerability_occurrence_identifiers: %w[project_id],
 | 
			
		||||
    vulnerability_scanners: %w[external_id],
 | 
			
		||||
    vulnerability_state_transitions: %w[state_changed_at_pipeline_id],
 | 
			
		||||
    security_scans: %w[pipeline_id project_id], # foreign key is not added as ci_pipeline table will be moved into different db soon
 | 
			
		||||
    dependency_list_exports: %w[pipeline_id], # foreign key is not added as ci_pipeline table is in different db
 | 
			
		||||
    vulnerability_reads: %w[cluster_agent_id namespace_id], # namespace_id is a denormalization of `project.namespace`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,6 +60,16 @@
 | 
			
		|||
    },
 | 
			
		||||
    "project": {
 | 
			
		||||
      "$ref": "project.json"
 | 
			
		||||
    },
 | 
			
		||||
    "cluster_agent": {
 | 
			
		||||
      "oneOf": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "null"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "$ref": "agent.json"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "additionalProperties": false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,21 @@
 | 
			
		|||
require "spec_helper"
 | 
			
		||||
 | 
			
		||||
RSpec.describe Import::PlaceholderReferences::AliasResolver, feature_category: :importers do
 | 
			
		||||
  describe "ALIASES" do
 | 
			
		||||
  describe ".aliases" do
 | 
			
		||||
    def missing_attribute_message(model, attribute)
 | 
			
		||||
      <<-MSG
 | 
			
		||||
        #{model}##{attribute} references a user and it is not defined in #{described_class}::ALIASES.
 | 
			
		||||
        Please add the attribute in the columns key in the #{described_class}::ALIASES['#{model}'] hash.
 | 
			
		||||
      MSG
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def missing_alias_message(model)
 | 
			
		||||
      <<-MSG
 | 
			
		||||
        #{model} models references a user and it is not defined in #{described_class}::ALIASES.
 | 
			
		||||
        Please define the mapping in #{described_class}::ALIASES.
 | 
			
		||||
      MSG
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "points to real columns" do
 | 
			
		||||
      def failure_message(model, column)
 | 
			
		||||
        <<-MSG
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +27,7 @@ RSpec.describe Import::PlaceholderReferences::AliasResolver, feature_category: :
 | 
			
		|||
        MSG
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      described_class::ALIASES.each_value.flat_map(&:values).each do |model_alias|
 | 
			
		||||
      described_class.aliases.each_value.flat_map(&:values).each do |model_alias|
 | 
			
		||||
        model = model_alias[:model]
 | 
			
		||||
        column_names = model.columns.map(&:name)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +36,86 @@ RSpec.describe Import::PlaceholderReferences::AliasResolver, feature_category: :
 | 
			
		|||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    shared_examples 'define aliases' do
 | 
			
		||||
      def relation_class(relation_key)
 | 
			
		||||
        relation_key.to_s.classify.constantize
 | 
			
		||||
      rescue NameError
 | 
			
		||||
        relation_key.to_s.constantize
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def extract_relation_names(hash, keys = [])
 | 
			
		||||
        keys += hash.keys
 | 
			
		||||
        hash.each_value do |value|
 | 
			
		||||
          keys += extract_relation_names(value, keys)
 | 
			
		||||
        end
 | 
			
		||||
        keys.uniq
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "defines aliases for imported resources that references users", :eager_load do
 | 
			
		||||
        relation_names = extract_relation_names(config_tree).reject { |name| ignore_relations.include?(name) }
 | 
			
		||||
        relation_names.each do |relation_name|
 | 
			
		||||
          relation_name = overrides[relation_name] || relation_name
 | 
			
		||||
          model_class = relation_class(relation_name)
 | 
			
		||||
          table_columns = model_class.columns.collect(&:name)
 | 
			
		||||
          user_associations = model_class.reflect_on_all_associations(:belongs_to)
 | 
			
		||||
            .reject(&:polymorphic?)
 | 
			
		||||
            .filter { |association| association.klass == User }
 | 
			
		||||
            .reject { |association| table_columns.exclude?(association.foreign_key) }
 | 
			
		||||
 | 
			
		||||
          next unless user_associations.any?
 | 
			
		||||
 | 
			
		||||
          expect(described_class.aliases[model_class.to_s]).to be_present, missing_alias_message(model_class)
 | 
			
		||||
 | 
			
		||||
          user_associations.each do |association|
 | 
			
		||||
            foreign_key = association.foreign_key
 | 
			
		||||
            last_version = described_class.aliases[model_class.to_s].keys.max
 | 
			
		||||
            alias_definition = described_class.aliases[model_class.to_s][last_version]
 | 
			
		||||
            expect(alias_definition[:columns]).to include(foreign_key),
 | 
			
		||||
              missing_attribute_message(model_class, foreign_key)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    describe 'group aliases' do
 | 
			
		||||
      let(:overrides) { Gitlab::ImportExport::Group::RelationFactory.overrides }
 | 
			
		||||
      let(:ignore_relations) { %i[members user_contributions author user] }
 | 
			
		||||
      let(:config_tree) do
 | 
			
		||||
        Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.group_config_file).to_h[:tree][:group]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'define aliases'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    describe 'project aliases' do
 | 
			
		||||
      let(:overrides) { Gitlab::ImportExport::Project::RelationFactory.overrides }
 | 
			
		||||
      let(:ignore_relations) { %i[project_members user_contributions author user] }
 | 
			
		||||
      let(:config_tree) do
 | 
			
		||||
        Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.config_file).to_h[:tree][:project]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'define aliases'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "defines aliases for all note descendants apart from synthetic notes" do
 | 
			
		||||
      user_associations = Note.reflect_on_all_associations(:belongs_to)
 | 
			
		||||
        .reject(&:polymorphic?)
 | 
			
		||||
        .filter { |association| association.klass == User }
 | 
			
		||||
        .reject { |association| Note.columns.collect(&:name).exclude?(association.foreign_key) }
 | 
			
		||||
 | 
			
		||||
      (Note.descendants - SyntheticNote.descendants - [SyntheticNote]).each do |descendant|
 | 
			
		||||
        expect(described_class.aliases[descendant.to_s]).to be_present, missing_alias_message(descendant)
 | 
			
		||||
 | 
			
		||||
        user_associations.each do |association|
 | 
			
		||||
          foreign_key = association.foreign_key
 | 
			
		||||
          last_version = described_class.aliases[descendant.to_s].keys.max
 | 
			
		||||
          alias_definition = described_class.aliases[descendant.to_s][last_version]
 | 
			
		||||
          expect(alias_definition[:columns]).to include(foreign_key),
 | 
			
		||||
            missing_attribute_message(descendant, foreign_key)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe ".version_for_model" do
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +135,7 @@ RSpec.describe Import::PlaceholderReferences::AliasResolver, feature_category: :
 | 
			
		|||
    end
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      stub_const("#{described_class}::ALIASES", aliases)
 | 
			
		||||
      allow(described_class).to receive(:aliases).and_return(aliases)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "returns the max version available for the model" do
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +174,7 @@ RSpec.describe Import::PlaceholderReferences::AliasResolver, feature_category: :
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      before do
 | 
			
		||||
        stub_const("#{described_class}::ALIASES", aliases)
 | 
			
		||||
        allow(described_class).to receive(:aliases).and_return(aliases)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "returns the value for the right version" do
 | 
			
		||||
| 
						 | 
				
			
			@ -106,7 +200,7 @@ RSpec.describe Import::PlaceholderReferences::AliasResolver, feature_category: :
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      before do
 | 
			
		||||
        stub_const("#{described_class}::ALIASES", aliases)
 | 
			
		||||
        allow(described_class).to receive(:aliases).and_return(aliases)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "returns the new model name" do
 | 
			
		||||
| 
						 | 
				
			
			@ -160,7 +254,7 @@ RSpec.describe Import::PlaceholderReferences::AliasResolver, feature_category: :
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      before do
 | 
			
		||||
        stub_const("#{described_class}::ALIASES", aliases)
 | 
			
		||||
        allow(described_class).to receive(:aliases).and_return(aliases)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "returns the value for the right version" do
 | 
			
		||||
| 
						 | 
				
			
			@ -184,7 +278,7 @@ RSpec.describe Import::PlaceholderReferences::AliasResolver, feature_category: :
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      before do
 | 
			
		||||
        stub_const("#{described_class}::ALIASES", aliases)
 | 
			
		||||
        allow(described_class).to receive(:aliases).and_return(aliases)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "returns the new column name" do
 | 
			
		||||
| 
						 | 
				
			
			@ -229,7 +323,7 @@ RSpec.describe Import::PlaceholderReferences::AliasResolver, feature_category: :
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      before do
 | 
			
		||||
        stub_const("#{described_class}::ALIASES", aliases)
 | 
			
		||||
        allow(described_class).to receive(:aliases).and_return(aliases)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "only includes the last version" do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,8 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
 | 
			
		|||
  let_it_be(:user) { create(:user) }
 | 
			
		||||
  let_it_be(:developer) { create(:user) }
 | 
			
		||||
  let_it_be(:non_member) { create(:user) }
 | 
			
		||||
  let_it_be(:project) { create(:project, :private, :repository, namespace: user.namespace, maintainers: user, developers: developer) }
 | 
			
		||||
  let_it_be(:reporter) { create(:user) }
 | 
			
		||||
  let_it_be(:project) { create(:project, :private, :repository, namespace: user.namespace, maintainers: user, developers: developer, reporters: reporter) }
 | 
			
		||||
  let_it_be_with_reload(:environment) { create(:environment, project: project) }
 | 
			
		||||
 | 
			
		||||
  describe 'GET /projects/:id/environments', :aggregate_failures do
 | 
			
		||||
| 
						 | 
				
			
			@ -383,11 +384,14 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
 | 
			
		|||
            deployable: job
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
          environment.update!(cluster_agent: create(:cluster_agent, project: project))
 | 
			
		||||
 | 
			
		||||
          get api("/projects/#{project.id}/environments/#{environment.id}", user)
 | 
			
		||||
 | 
			
		||||
          expect(response).to have_gitlab_http_status(:ok)
 | 
			
		||||
          expect(response).to match_response_schema('public_api/v4/environment')
 | 
			
		||||
          expect(json_response['last_deployment']).to be_present
 | 
			
		||||
          expect(json_response['cluster_agent']).to be_present
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -443,6 +447,18 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'as a reporter' do
 | 
			
		||||
      it 'does not expose the cluster agent' do
 | 
			
		||||
        environment.update!(cluster_agent: create(:cluster_agent, project: project))
 | 
			
		||||
 | 
			
		||||
        get api("/projects/#{project.id}/environments/#{environment.id}", reporter)
 | 
			
		||||
 | 
			
		||||
        expect(response).to have_gitlab_http_status(:ok)
 | 
			
		||||
        expect(response).to match_response_schema('public_api/v4/environment')
 | 
			
		||||
        expect(json_response['cluster_agent']).not_to be_present
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'as non member' do
 | 
			
		||||
      shared_examples 'environment will not be found' do
 | 
			
		||||
        it 'returns a 404 status code' do
 | 
			
		||||
| 
						 | 
				
			
			@ -518,12 +534,6 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
 | 
			
		|||
    end
 | 
			
		||||
 | 
			
		||||
    context "as a reporter" do
 | 
			
		||||
      let(:reporter) { create(:user) }
 | 
			
		||||
 | 
			
		||||
      before do
 | 
			
		||||
        project.add_reporter(reporter)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "rejects the request" do
 | 
			
		||||
        delete api("/projects/#{project.id}/environments/review_apps", reporter)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,7 @@ RSpec.describe Import::SourceUsersController, feature_category: :importers do
 | 
			
		|||
 | 
			
		||||
      expect { subject }.not_to change { source_user.reload.status }
 | 
			
		||||
 | 
			
		||||
      expect(response).to redirect_to(dashboard_groups_path)
 | 
			
		||||
      expect(response).to redirect_to(root_path)
 | 
			
		||||
      expect(flash[:raw]).to match(/Reassignment not available/)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +42,7 @@ RSpec.describe Import::SourceUsersController, feature_category: :importers do
 | 
			
		|||
 | 
			
		||||
      expect { subject }.not_to change { source_user.reload.status }
 | 
			
		||||
 | 
			
		||||
      expect(response).to redirect_to(dashboard_groups_path)
 | 
			
		||||
      expect(response).to redirect_to(root_path)
 | 
			
		||||
      expect(flash[:raw]).to match(/Reassignment not available/)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +72,7 @@ RSpec.describe Import::SourceUsersController, feature_category: :importers do
 | 
			
		|||
      it 'redirects with a notice when accepted' do
 | 
			
		||||
        accept_invite
 | 
			
		||||
 | 
			
		||||
        expect(response).to redirect_to(dashboard_groups_path)
 | 
			
		||||
        expect(response).to redirect_to(root_path)
 | 
			
		||||
        expect(flash[:raw]).to match(/Reassignment approved/)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +82,7 @@ RSpec.describe Import::SourceUsersController, feature_category: :importers do
 | 
			
		|||
 | 
			
		||||
        accept_invite
 | 
			
		||||
 | 
			
		||||
        expect(response).to redirect_to(dashboard_groups_path)
 | 
			
		||||
        expect(response).to redirect_to(root_path)
 | 
			
		||||
        expect(flash[:alert]).to match(/The invitation could not be accepted/)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +112,7 @@ RSpec.describe Import::SourceUsersController, feature_category: :importers do
 | 
			
		|||
      it 'redirects with a notice' do
 | 
			
		||||
        reject_invite
 | 
			
		||||
 | 
			
		||||
        expect(response).to redirect_to(dashboard_groups_path)
 | 
			
		||||
        expect(response).to redirect_to(root_path)
 | 
			
		||||
        expect(flash[:raw]).to match(/Reassignment rejected/)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +122,7 @@ RSpec.describe Import::SourceUsersController, feature_category: :importers do
 | 
			
		|||
 | 
			
		||||
        reject_invite
 | 
			
		||||
 | 
			
		||||
        expect(response).to redirect_to(dashboard_groups_path)
 | 
			
		||||
        expect(response).to redirect_to(root_path)
 | 
			
		||||
        expect(flash[:alert]).to match(/The invitation could not be declined/)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue