インポート/エクスポート開発ドキュメント

インポート/エクスポート機能の一般的な開発ガイドラインとヒント。

この文書は、YouTubeで公開されているインポート/エクスポート201プレゼンテーションに基づいています。

セキュリティ

インポート/エクスポート機能は常に更新されています(エクスポートする新しいものが追加されています)。しかし、コードは長い間リファクタリングされていません。その動的な性質がセキュリティ上の懸念を増加させないことを確認するために、コード監査を行う必要があります。GitLabチームメンバーはこの機密イシューでより多くの情報を見ることができます:https://gitlab.com/gitlab-org/gitlab/-/issues/20720.

コードのセキュリティ

これらのクラスの中には、インポート/エクスポートにセキュリティのレイヤーを提供するものがあります。

AttributeCleaner は、禁止されているキーを削除します:

# AttributeCleaner
# Removes all `_ids` and other prohibited keys
    class AttributeCleaner
      ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']

      def clean
        @relation_hash.reject do |key, _value|
          prohibited_key?(key) || !@relation_class.attribute_method?(key) || excluded_key?(key)
        end.except('id')
      end

      ...

AttributeConfigurationSpec は新しいカラムの追加をチェックし、確認します:

# AttributeConfigurationSpec
<<-MSG
  It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes:

  Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if they can be exported.

  Please denylist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its corresponding
  model in the +excluded_attributes+ section.

  SAFE_MODEL_ATTRIBUTES: #{File.expand_path(safe_attributes_file)}
  IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
MSG

ModelConfigurationSpec は、新しいモデルの追加をチェックし、確認します:

# ModelConfigurationSpec
<<-MSG
  New model(s) <#{new_models.join(',')}> have been added, related to #{parent_model_name}, which is exported by
  the Import/Export feature.

  If you think this model should be included in the export, please add it to `#{Gitlab::ImportExport.config_file}`.

  Definitely add it to `#{File.expand_path(ce_models_yml)}`
  to signal that you've handled this error and to prevent it from showing up in the future.
MSG

ExportFileSpec は暗号化された列や機密性の高い列を検出します:

# ExportFileSpec
<<-MSG
  Found a new sensitive word <#{key_found}>, which is part of the hash #{parent.inspect}
  If you think this information shouldn't get exported, please exclude the model or attribute in
  IMPORT_EXPORT_CONFIG.

  Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the
  key and the correspondent hash or model as the value.

  Also, if the attribute is a generated unique token, please add it to RelationFactory::TOKEN_RESET_MODELS
  if it needs to be reset (to prevent duplicate column problems while importing to the same instance).

  IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
  CURRENT_SPEC: #{__FILE__}
MSG

バージョニング

インポート/エクスポートは、1つのGitLabリリース中に頻繁に一定の変更があるため、厳密なSemVerを使用しません。ただし、変更があった場合はアップデートが必要です。

# ImportExport
module Gitlab
  module ImportExport
    extend self

    # For every version update, the version history in import_export.md has to be kept up to date.
    VERSION = '0.2.4'

互換性

プロジェクトのインポートおよびエクスポート時に互換性をチェックします。

バージョンを上げるタイミング

モデルやカラムの名前を変更したり、何らかのフォーマットを行ったりした場合、JSON構造体やアーカイブファイルのファイル構造体のバージョン修正をバンプする必要があります。

以下のような場合は、バージョンを上げる必要はありません:

  • 新しいカラムもしくはモデルの追加
  • カラムもしくはモデルを削除します (DB の制約がある場合を除く)
  • 新しいエクスポート (新しいタイプのアップロードなど)

バージョンを上げるたびに、インテグレーションが失敗します:

bundle exec rake gitlab:import_export:bump_version

コードに素早く飛び込む

インポート/エクスポート設定 (import_export.yml)

メイン設定import_export.yml は、エクスポート/インポートできるモデルを定義します。

プロジェクトのインポート/エクスポートに含まれるモデルの関係:

project_tree:
  - labels:
    - :priorities
  - milestones:
    - events:
      - :push_event_payload
  - issues:
    - events:
    # ...

指定されたモデルの以下の属性のみを含めます:

included_attributes:
  user:
    - :id
    - :public_email
  # ...

指定されたモデルに対して以下の属性を含めないでください:

excluded_attributes:
  project:
    - :name
    - :path
    - ...

エクスポートによって呼び出されるExporterメソッド:

# Methods
methods:
  labels:
    - :type
  label:
    - :type

モデルのリレーションシップのエクスポート順序をカスタマイズします:

# Specify a custom export reordering for a given relationship
# For example for issues we use a custom export reordering by relative_position, so that on import, we can reset the
# relative position value, but still keep the issues order to the order in which issues were in the exported project.
# By default the ordering of relations is done by PK.
# column - specify the column by which to reorder, by default it is relation's PK
# direction - specify the ordering direction :asc or :desc, default :asc
# nulls_position - specify where would null values be positioned. Because custom ordering column can contain nulls we
#                  need to also specify where would the nulls be placed. It can be :nulls_last or :nulls_first, defaults
#                  to :nulls_last

export_reorders:
  project:
    issues:
      column: :relative_position
      direction: :asc
      nulls_position: :nulls_last

条件付きエクスポート

関連付けられたリソースがプロジェクト外部のものである場合、プロジェクトやグループをエクスポートするユーザーがこれらの関連付けにアクセスできるかどうかを検証する必要があるかもしれません。include_if_exportable はリソースの関連付けの配列を受け入れます。エクスポート中に、リソースのexportable_association? メソッドがアソシエーション名とユーザー名で呼び出され、関連リソースがエクスポートに含まれるかどうかを検証します。

使用例:

include_if_exportable:
  project:
    issues:
      - epic_issue

この定義

  1. イシューのexportable_association?(:epic_issue, current_user: current_user) メソッドを呼び出します。
  2. このメソッドがtrueを返す場合、issueのepic_issue 関連付けをイシューに含めます。

インポート

インポートジョブのステータスは、none からfinished またはfailed へと移行します:

import_status: none -> scheduled -> started -> finished/failed

ステータスがstarted である間、Importer コードはインポートに必要な各ステップを処理します。

# ImportExport::Importer
module Gitlab
  module ImportExport
    class Importer
      def execute
        if import_file && check_version! && restorers.all?(&:restore) && overwrite_project
          project
        else
          raise Projects::ImportService::Error.new(@shared.errors.join(', '))
        end
      rescue => e
        raise Projects::ImportService::Error.new(e.message)
      ensure
        remove_import_file
      end

      def restorers
        [repo_restorer, wiki_restorer, project_tree, avatar_restorer,
         uploads_restorer, lfs_restorer, statistics_restorer]
      end

エクスポートサービスは、Importer に似ていますが、データを保存する代わりにデータを復元します。

エクスポート

# ImportExport::ExportService
module Projects
  module ImportExport
    class ExportService < BaseService

      def save_all!
        if save_services
          Gitlab::ImportExport::Saver.save(project: project, shared: @shared)
          notify_success
        else
          cleanup_and_notify_error!
        end
      end

      def save_services
        [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver,
           wiki_repo_saver, lfs_saver].all?(&:save)
      end

テストフィクスチャ

インポート/エクスポート仕様で使われるフィクスチャはspec/fixtures/lib/gitlab/import_export にあります。プロジェクト・フィクスチャとグループ・フィクスチャの両方があります。

これらのフィクスチャにはそれぞれ2つのバージョンがあります:

  • project.json もしくはgroup.json と呼ばれる、すべてのオブジェクトを含む、人間が読める単一のJSONファイル。
  • ndjson フォーマットのファイルのツリーを含むtree という名前のフォルダ。厳密な必要性がない限り、このフォルダの下のファイルを手動で編集しないでください。

人間が読めるJSONファイルからNDJSONツリーを生成するツールは、gitlab-org/memory-team/team-tools プロジェクトにあります。

プロジェクト

** legacy-project-json-to-ndjson.sh 、NDJSONツリーを生成してください。**

NDJSONツリーは以下のようになります:

tree
├── project
│   ├── auto_devops.ndjson
│   ├── boards.ndjson
│   ├── ci_cd_settings.ndjson
│   ├── ci_pipelines.ndjson
│   ├── container_expiration_policy.ndjson
│   ├── custom_attributes.ndjson
│   ├── error_tracking_setting.ndjson
│   ├── external_pull_requests.ndjson
│   ├── issues.ndjson
│   ├── labels.ndjson
│   ├── merge_requests.ndjson
│   ├── milestones.ndjson
│   ├── pipeline_schedules.ndjson
│   ├── project_badges.ndjson
│   ├── project_feature.ndjson
│   ├── project_members.ndjson
│   ├── protected_branches.ndjson
│   ├── protected_tags.ndjson
│   ├── releases.ndjson
│   ├── services.ndjson
│   ├── snippets.ndjson
│   └── triggers.ndjson
└── project.json

グループ

legacy-group-json-to-ndjson.rb 、NDJSONツリーを生成してください。

NDJSONツリーは以下のようになります:

tree
└── groups
    ├── 4351
    │   ├── badges.ndjson
    │   ├── boards.ndjson
    │   ├── epics.ndjson
    │   ├── labels.ndjson
    │   ├── members.ndjson
    │   └── milestones.ndjson
    ├── 4352
    │   ├── badges.ndjson
    │   ├── boards.ndjson
    │   ├── epics.ndjson
    │   ├── labels.ndjson
    │   ├── members.ndjson
    │   └── milestones.ndjson
    ├── _all.ndjson
    ├── 4351.json
    └── 4352.json
caution
これらのフィクスチャを更新するとき、テストが両方に適用されるので、json ファイルとtree フォルダの両方を更新することを確認してください。