From a916e4d4fa9ca546b5e8f6b0b6f26446b7cbe266 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Fri, 8 Mar 2019 06:44:20 +0000
Subject: [PATCH] Issue #2951550 by mikelutz, Jo Fitzgerald, quietone, heddn,
 alexpott, phenaproxima: Make service for field discovery for use in migrate
 entity derivers

---
 .../comment/src/Plugin/migrate/D7Comment.php  |  31 +-
 .../migrate_drupal.services.yml               |   9 +
 .../migrate_drupal/src/FieldDiscovery.php     | 385 ++++++++++++++++++
 .../src/FieldDiscoveryInterface.php           |  68 ++++
 .../src/Plugin/migrate/FieldMigration.php     |  96 +----
 .../field_discovery_test.info.yml             |   6 +
 .../src/FieldDiscoveryTestClass.php           |  93 +++++
 .../src/Kernel/d6/FieldDiscoveryTest.php      | 304 ++++++++++++++
 .../src/Kernel/d7/FieldDiscoveryTest.php      | 364 +++++++++++++++++
 .../src/Traits/FieldDiscoveryTestTrait.php    | 103 +++++
 .../tests/src/Unit/FieldDiscoveryTest.php     | 358 ++++++++++++++++
 .../node/src/Plugin/migrate/D6NodeDeriver.php | 106 +----
 .../node/src/Plugin/migrate/D7NodeDeriver.php |  99 +----
 .../Plugin/migrate/D7TaxonomyTermDeriver.php  |  92 +----
 core/modules/user/src/Plugin/migrate/User.php |  54 +--
 15 files changed, 1760 insertions(+), 408 deletions(-)
 create mode 100644 core/modules/migrate_drupal/src/FieldDiscovery.php
 create mode 100644 core/modules/migrate_drupal/src/FieldDiscoveryInterface.php
 create mode 100644 core/modules/migrate_drupal/tests/modules/field_discovery_test/field_discovery_test.info.yml
 create mode 100644 core/modules/migrate_drupal/tests/modules/field_discovery_test/src/FieldDiscoveryTestClass.php
 create mode 100644 core/modules/migrate_drupal/tests/src/Kernel/d6/FieldDiscoveryTest.php
 create mode 100644 core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php
 create mode 100644 core/modules/migrate_drupal/tests/src/Traits/FieldDiscoveryTestTrait.php
 create mode 100644 core/modules/migrate_drupal/tests/src/Unit/FieldDiscoveryTest.php

diff --git a/core/modules/comment/src/Plugin/migrate/D7Comment.php b/core/modules/comment/src/Plugin/migrate/D7Comment.php
index a162400688fe..82590b4559b7 100644
--- a/core/modules/comment/src/Plugin/migrate/D7Comment.php
+++ b/core/modules/comment/src/Plugin/migrate/D7Comment.php
@@ -13,34 +13,9 @@ class D7Comment extends FieldMigration {
    * {@inheritdoc}
    */
   public function getProcess() {
-    if ($this->init) {
-      return parent::getProcess();
-    }
-    $this->init = TRUE;
-    if (!\Drupal::moduleHandler()->moduleExists('field')) {
-      return parent::getProcess();
-    }
-    $definition['source'] = [
-      'ignore_map' => TRUE,
-    ] + $this->getSourceConfiguration();
-    $definition['source']['plugin'] = 'd7_field_instance';
-    $definition['destination']['plugin'] = 'null';
-    $definition['idMap']['plugin'] = 'null';
-    $field_migration = $this->migrationPluginManager->createStubMigration($definition);
-    foreach ($field_migration->getSourcePlugin() as $row) {
-      $field_name = $row->getSourceProperty('field_name');
-      $field_type = $row->getSourceProperty('type');
-      if ($this->fieldPluginManager->hasDefinition($field_type)) {
-        if (!isset($this->fieldPluginCache[$field_type])) {
-          $plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, [], $this);
-          $this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($plugin_id, [], $this);
-        }
-        $info = $row->getSource();
-        $this->fieldPluginCache[$field_type]->defineValueProcessPipeline($this, $field_name, $info);
-      }
-      else {
-        $this->setProcessOfProperty($field_name, $field_name);
-      }
+    if (!$this->init) {
+      $this->init = TRUE;
+      $this->fieldDiscovery->addEntityFieldProcesses($this, 'comment');
     }
     return parent::getProcess();
   }
diff --git a/core/modules/migrate_drupal/migrate_drupal.services.yml b/core/modules/migrate_drupal/migrate_drupal.services.yml
index 23b3492baf90..7594b465fefb 100644
--- a/core/modules/migrate_drupal/migrate_drupal.services.yml
+++ b/core/modules/migrate_drupal/migrate_drupal.services.yml
@@ -16,3 +16,12 @@ services:
       - '@module_handler'
       - '\Drupal\migrate_drupal\Annotation\MigrateCckField'
     deprecated: The "%service_id%" service is deprecated. You should use the 'plugin.manager.migrate.field' service instead. See https://www.drupal.org/node/2751897
+  logger.channel.migrate_drupal:
+    parent: logger.channel_base
+    arguments: ['migrate_drupal']
+  migrate_drupal.field_discovery:
+    class: Drupal\migrate_drupal\FieldDiscovery
+    arguments:
+      - '@plugin.manager.migrate.field'
+      - '@plugin.manager.migration'
+      - '@logger.channel.migrate_drupal'
diff --git a/core/modules/migrate_drupal/src/FieldDiscovery.php b/core/modules/migrate_drupal/src/FieldDiscovery.php
new file mode 100644
index 000000000000..6f6fdba45d69
--- /dev/null
+++ b/core/modules/migrate_drupal/src/FieldDiscovery.php
@@ -0,0 +1,385 @@
+<?php
+
+namespace Drupal\migrate_drupal;
+
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Core\Logger\LoggerChannelInterface;
+use Drupal\migrate\Exception\RequirementsException;
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
+use Drupal\migrate\Plugin\RequirementsInterface;
+use Drupal\migrate_drupal\Plugin\MigrateCckFieldInterface;
+use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
+
+/**
+ * Provides field discovery for Drupal 6 & 7 migrations.
+ */
+class FieldDiscovery implements FieldDiscoveryInterface {
+
+  /**
+   * The CCK plugin manager.
+   *
+   * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface
+   */
+  protected $cckPluginManager;
+
+  /**
+   * An array of already discovered field plugin information.
+   *
+   * @var array
+   */
+  protected $fieldPluginCache;
+
+  /**
+   * The field plugin manager.
+   *
+   * @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
+   */
+  protected $fieldPluginManager;
+
+  /**
+   * The migration plugin manager.
+   *
+   * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
+   */
+  protected $migrationPluginManager;
+
+  /**
+   * The logger channel service.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelInterface
+   */
+  protected $logger;
+
+  /**
+   * A cache of discovered fields.
+   *
+   * It is an array of arrays. If the entity type is bundleable, a third level
+   * of arrays is added to account for fields discovered at the bundle level.
+   *
+   * [{core}][{entity_type}][{bundle}]
+   *
+   * @var array
+   */
+  protected $discoveredFieldsCache = [];
+
+  /**
+   * An array of bundle keys, keyed by drupal core version.
+   *
+   * In Drupal 6, only nodes were fieldable, and the bundles were called
+   * 'type_name'.  In Drupal 7, everything became entities, and the more
+   * generic 'bundle' was used.
+   *
+   * @var array
+   */
+  protected $bundleKeys = [
+    FieldDiscoveryInterface::DRUPAL_6 => 'type_name',
+    FieldDiscoveryInterface::DRUPAL_7 => 'bundle',
+  ];
+
+  /**
+   * An array of source plugin ids, keyed by Drupal core version.
+   *
+   * @var array
+   */
+  protected $sourcePluginIds = [
+    FieldDiscoveryInterface::DRUPAL_6 => 'd6_field_instance',
+    FieldDiscoveryInterface::DRUPAL_7 => 'd7_field_instance',
+  ];
+
+  /**
+   * An array of supported Drupal core versions.
+   *
+   * @var array
+   */
+  protected $supportedCoreVersions = [
+    FieldDiscoveryInterface::DRUPAL_6,
+    FieldDiscoveryInterface::DRUPAL_7,
+  ];
+
+  /**
+   * Constructs a FieldDiscovery object.
+   *
+   * @param \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface $field_plugin_manager
+   *   The field plugin manager.
+   * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
+   *   The migration plugin manager.
+   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
+   *   The logger channel service.
+   */
+  public function __construct(MigrateFieldPluginManagerInterface $field_plugin_manager, MigrationPluginManagerInterface $migration_plugin_manager, LoggerChannelInterface $logger) {
+    $this->fieldPluginManager = $field_plugin_manager;
+    $this->migrationPluginManager = $migration_plugin_manager;
+    $this->logger = $logger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addAllFieldProcesses(MigrationInterface $migration) {
+    $core = $this->getCoreVersion($migration);
+    $fields = $this->getAllFields($core);
+    foreach ($fields as $entity_type_id => $bundle) {
+      $this->addEntityFieldProcesses($migration, $entity_type_id);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addEntityFieldProcesses(MigrationInterface $migration, $entity_type_id) {
+    $core = $this->getCoreVersion($migration);
+    $fields = $this->getAllFields($core);
+    if (!empty($fields[$entity_type_id])  && is_array($fields[$entity_type_id])) {
+      foreach ($fields[$entity_type_id] as $bundle => $fields) {
+        $this->addBundleFieldProcesses($migration, $entity_type_id, $bundle);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addBundleFieldProcesses(MigrationInterface $migration, $entity_type_id, $bundle) {
+    $core = $this->getCoreVersion($migration);
+    $fields = $this->getAllFields($core);
+    $plugin_definition = $migration->getPluginDefinition();
+    if (empty($fields[$entity_type_id][$bundle])) {
+      return;
+    }
+    $bundle_fields = $fields[$entity_type_id][$bundle];
+    foreach ($bundle_fields as $field_name => $field_info) {
+      $plugin = $this->getFieldPlugin($field_info['type'], $migration);
+      if ($plugin) {
+        $method = isset($plugin_definition['field_plugin_method']) ? $plugin_definition['field_plugin_method'] : 'defineValueProcessPipeline';
+
+        // @todo Remove the following 3 lines of code prior to Drupal 9.0.0.
+        // https://www.drupal.org/node/3032317
+        if ($plugin instanceof MigrateCckFieldInterface) {
+          $method = isset($plugin_definition['cck_plugin_method']) ? $plugin_definition['cck_plugin_method'] : 'processCckFieldValues';
+        }
+
+        call_user_func_array([
+          $plugin,
+          $method,
+        ], [
+          $migration,
+          $field_name,
+          $field_info,
+        ]);
+      }
+      else {
+        // Default to a get process plugin if this is a value migration.
+        if ((empty($plugin_definition['field_plugin_method']) || $plugin_definition['field_plugin_method'] === 'defineValueProcessPipeline') && (empty($plugin_definition['cck_plugin_method']) || $plugin_definition['cck_plugin_method'] === 'processCckFieldValues')) {
+          $migration->setProcessOfProperty($field_name, $field_name);
+        }
+      }
+    }
+  }
+
+  /**
+   * Returns the appropriate field plugin for a given field type.
+   *
+   * @param string $field_type
+   *   The field type.
+   * @param \Drupal\migrate\Plugin\MigrationInterface $migration
+   *   The migration to retrieve the plugin for.
+   *
+   * @return \Drupal\migrate_drupal\Plugin\MigrateCckFieldInterface|\Drupal\migrate_drupal\Plugin\MigrateFieldInterface|bool
+   *   The appropriate field or cck plugin to process this field type.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   * @throws \InvalidArgumentException
+   */
+  protected function getFieldPlugin($field_type, MigrationInterface $migration) {
+    $core = $this->getCoreVersion($migration);
+    if (!isset($this->fieldPluginCache[$core][$field_type])) {
+      try {
+        $plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, ['core' => $core], $migration);
+        $plugin = $this->fieldPluginManager->createInstance($plugin_id, ['core' => $core], $migration);
+      }
+      catch (PluginNotFoundException $ex) {
+        // @todo Replace try/catch block with $plugin = FALSE for Drupal 9.
+        // https://www.drupal.org/project/drupal/issues/3033733
+        try {
+          /** @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManager $cck_plugin_manager */
+          $cck_plugin_manager = $this->getCckPluginManager();
+          $plugin_id = $cck_plugin_manager->getPluginIdFromFieldType($field_type, ['core' => $core], $migration);
+          $plugin = $cck_plugin_manager->createInstance($plugin_id, ['core' => $core], $migration);
+        }
+        catch (PluginNotFoundException $ex) {
+          $plugin = FALSE;
+        }
+      }
+      $this->fieldPluginCache[$core][$field_type] = $plugin;
+    }
+    return $this->fieldPluginCache[$core][$field_type];
+  }
+
+  /**
+   * Gets all field information related to this migration.
+   *
+   * @param string $core
+   *   The Drupal core version to get fields for.
+   *
+   * @return array
+   *   A multidimensional array of source data from the relevant field instance
+   *   migration, keyed first by entity type, then by bundle and finally by
+   *   field name.
+   */
+  protected function getAllFields($core) {
+    if (empty($this->discoveredFieldsCache[$core])) {
+      $this->discoveredFieldsCache[$core] = [];
+      $source_plugin = $this->getSourcePlugin($core);
+      foreach ($source_plugin as $row) {
+        /** @var \Drupal\migrate\Row $row */
+        if ($core === FieldDiscoveryInterface::DRUPAL_7) {
+          $entity_type_id = $row->get('entity_type');
+        }
+        else {
+          $entity_type_id = 'node';
+        }
+        $bundle = $row->getSourceProperty($this->bundleKeys[$core]);
+        $this->discoveredFieldsCache[$core][$entity_type_id][$bundle][$row->getSourceProperty('field_name')] = $row->getSource();
+      }
+    }
+    return $this->discoveredFieldsCache[$core];
+  }
+
+  /**
+   * Gets all field information for a particular entity type.
+   *
+   * @param string $core
+   *   The Drupal core version.
+   * @param string $entity_type_id
+   *   The legacy entity type ID.
+   *
+   * @return array
+   *   A multidimensional array of source data from the relevant field instance
+   *   migration for the entity type, keyed first by bundle and then by field
+   *   name.
+   */
+  protected function getEntityFields($core, $entity_type_id) {
+    $fields = $this->getAllFields($core);
+    if (!empty($fields[$entity_type_id])) {
+      return $fields[$entity_type_id];
+    }
+    return [];
+  }
+
+  /**
+   * Gets all field information for a particular entity type and bundle.
+   *
+   * @param string $core
+   *   The Drupal core version.
+   * @param string $entity_type_id
+   *   The legacy entity type ID.
+   * @param string $bundle
+   *   The legacy bundle (or content_type).
+   *
+   * @return array
+   *   An array of source data from the relevant field instance migration for
+   *   the bundle, keyed by field name.
+   */
+  protected function getBundleFields($core, $entity_type_id, $bundle) {
+    $fields = $this->getEntityFields($core, $entity_type_id);
+    if (!empty($fields[$bundle])) {
+      return $fields[$bundle];
+    }
+    return [];
+  }
+
+  /**
+   * Gets the deprecated CCK Plugin Manager service as a BC shim.
+   *
+   * We don't inject this service directly because it is deprecated, and we
+   * don't want to instantiate the plugin manager unless we have to, to avoid
+   * triggering deprecation errors.
+   *
+   * @return \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface
+   *   The CCK Plugin Manager.
+   */
+  protected function getCckPluginManager() {
+    if (!$this->cckPluginManager) {
+      $this->cckPluginManager = \Drupal::service('plugin.manager.migrate.cckfield');
+    }
+    return $this->cckPluginManager;
+  }
+
+  /**
+   * Gets the source plugin to use to gather field information.
+   *
+   * @param string $core
+   *   The Drupal core version.
+   *
+   * @return array|\Drupal\migrate\Plugin\MigrateSourceInterface
+   *   The source plugin, or an empty array if none can be found that meets
+   *   requirements.
+   */
+  protected function getSourcePlugin($core) {
+    $definition = $this->getFieldInstanceStubMigrationDefinition($core);
+    $source_plugin = $this->migrationPluginManager
+      ->createStubMigration($definition)
+      ->getSourcePlugin();
+    if ($source_plugin instanceof RequirementsInterface) {
+      try {
+        $source_plugin->checkRequirements();
+      }
+      catch (RequirementsException $e) {
+        // If checkRequirements() failed, the source database did not support
+        // fields (i.e., CCK is not installed in D6 or Field is not installed in
+        // D7). Therefore, $fields will be empty and below we'll return an empty
+        // array. The migration will proceed without adding fields.
+        $this->logger->notice('Field discovery failed for Drupal core version @core. Did this site have the CCK or Field module installed? Error: @message', [
+          '@core' => $core,
+          '@message' => $e->getMessage(),
+        ]);
+        return [];
+      }
+    }
+    return $source_plugin;
+  }
+
+  /**
+   * Provides the stub migration definition for a given Drupal core version.
+   *
+   * @param string $core
+   *   The Drupal core version.
+   *
+   * @return array
+   *   The stub migration definition.
+   */
+  protected function getFieldInstanceStubMigrationDefinition($core) {
+    return [
+      'destination' => ['plugin' => 'null'],
+      'idMap' => ['plugin' => 'null'],
+      'source' => [
+        'ignore_map' => TRUE,
+        'plugin' => $this->sourcePluginIds[$core],
+      ],
+    ];
+  }
+
+  /**
+   * Finds the core version of a Drupal migration.
+   *
+   * @param \Drupal\migrate\Plugin\MigrationInterface $migration
+   *   The migration.
+   *
+   * @return string|bool
+   *   A string representation of the Drupal version, or FALSE.
+   *
+   * @throws \InvalidArgumentException
+   */
+  protected function getCoreVersion(MigrationInterface $migration) {
+    $tags = $migration->getMigrationTags();
+    if (in_array('Drupal 7', $tags, TRUE)) {
+      return FieldDiscoveryInterface::DRUPAL_7;
+    }
+    elseif (in_array('Drupal 6', $tags, TRUE)) {
+      return FieldDiscoveryInterface::DRUPAL_6;
+    }
+    throw new \InvalidArgumentException("Drupal Core version not found for this migration");
+  }
+
+}
diff --git a/core/modules/migrate_drupal/src/FieldDiscoveryInterface.php b/core/modules/migrate_drupal/src/FieldDiscoveryInterface.php
new file mode 100644
index 000000000000..5648f219eb69
--- /dev/null
+++ b/core/modules/migrate_drupal/src/FieldDiscoveryInterface.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\migrate_drupal;
+
+use Drupal\migrate\Plugin\MigrationInterface;
+
+/**
+ * Provides field discovery for Drupal 6 & 7 migrations.
+ */
+interface FieldDiscoveryInterface {
+
+  const DRUPAL_6 = '6';
+
+  const DRUPAL_7 = '7';
+
+  /**
+   * Adds the field processes to a migration.
+   *
+   * This method is used in field migrations to execute the migration process
+   * alter method specified by the 'field_plugin_method' key of the migration
+   * for all field plugins applicable to this Drupal to Drupal migration. This
+   * method is used internally for field, field instance, widget, and formatter
+   * migrations to allow field plugins to alter the process for these
+   * migrations.
+   *
+   * @param \Drupal\migrate\Plugin\MigrationInterface $migration
+   *   The migration to add process plugins to.
+   *
+   * @throws \InvalidArgumentException
+   *
+   * @internal
+   */
+  public function addAllFieldProcesses(MigrationInterface $migration);
+
+  /**
+   * Adds the field processes for an entity to a migration.
+   *
+   * This method is used in field migrations to execute the migration process
+   * alter method specified by the 'field_plugin_method' key of the migration
+   * for all field plugins applicable to this Drupal to Drupal migration. This
+   * method is used internally for field, field instance, widget, and formatter
+   * migrations to allow field plugins to alter the process for these
+   * migrations.
+   *
+   * @param \Drupal\migrate\Plugin\MigrationInterface $migration
+   *   The migration to add processes to.
+   * @param string $entity_type_id
+   *   The legacy entity type to add processes for.
+   *
+   * @throws \InvalidArgumentException
+   */
+  public function addEntityFieldProcesses(MigrationInterface $migration, $entity_type_id);
+
+  /**
+   * Adds the field processes for a bundle to a migration.
+   *
+   * @param \Drupal\migrate\Plugin\MigrationInterface $migration
+   *   The migration to add processes to.
+   * @param string $entity_type_id
+   *   The legacy entity type to add processes for.
+   * @param string $bundle
+   *   The legacy bundle (or content_type) to add processes for.
+   *
+   * @throws \InvalidArgumentException
+   */
+  public function addBundleFieldProcesses(MigrationInterface $migration, $entity_type_id, $bundle);
+
+}
diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/FieldMigration.php b/core/modules/migrate_drupal/src/Plugin/migrate/FieldMigration.php
index 3ca8f26a8bbe..f84bdb4a0382 100644
--- a/core/modules/migrate_drupal/src/Plugin/migrate/FieldMigration.php
+++ b/core/modules/migrate_drupal/src/Plugin/migrate/FieldMigration.php
@@ -2,16 +2,12 @@
 
 namespace Drupal\migrate_drupal\Plugin\migrate;
 
-use Drupal\Component\Plugin\Exception\PluginNotFoundException;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\migrate\Exception\RequirementsException;
 use Drupal\migrate\Plugin\MigrateDestinationPluginManager;
 use Drupal\migrate\Plugin\MigratePluginManager;
 use Drupal\migrate\Plugin\Migration;
 use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
-use Drupal\migrate\Plugin\RequirementsInterface;
-use Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface;
-use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
+use Drupal\migrate_drupal\FieldDiscoveryInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -27,6 +23,7 @@ class FieldMigration extends Migration implements ContainerFactoryPluginInterfac
    * fallback to the old 'cck_plugin_method'.
    *
    * @const string
+   * @deprecated This constant is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use the migrate_drupal.field_discovery service instead. See https://www.drupal.org/node/3006076.
    */
   const PLUGIN_METHOD = 'field_plugin_method';
 
@@ -38,39 +35,11 @@ class FieldMigration extends Migration implements ContainerFactoryPluginInterfac
   protected $init = FALSE;
 
   /**
-   * List of field plugin IDs which have already run.
+   * The migration field discovery service.
    *
-   * @var string[]
+   * @var \Drupal\migrate_drupal\FieldDiscoveryInterface
    */
-  protected $processedFieldTypes = [];
-
-  /**
-   * Already-instantiated field plugins, keyed by ID.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateFieldInterface[]
-   */
-  protected $fieldPluginCache;
-
-  /**
-   * The field plugin manager.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
-   */
-  protected $fieldPluginManager;
-
-  /**
-   * Already-instantiated cckfield plugins, keyed by ID.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldInterface[]
-   */
-  protected $cckPluginCache;
-
-  /**
-   * The cckfield plugin manager.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface
-   */
-  protected $cckPluginManager;
+  protected $fieldDiscovery;
 
   /**
    * Constructs a FieldMigration.
@@ -81,10 +50,6 @@ class FieldMigration extends Migration implements ContainerFactoryPluginInterfac
    *   The plugin ID.
    * @param mixed $plugin_definition
    *   The plugin definition.
-   * @param \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface $cck_manager
-   *   The cckfield plugin manager.
-   * @param \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface $field_manager
-   *   The field plugin manager.
    * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
    *   The migration plugin manager.
    * @param \Drupal\migrate\Plugin\MigratePluginManager $source_plugin_manager
@@ -95,11 +60,12 @@ class FieldMigration extends Migration implements ContainerFactoryPluginInterfac
    *   The destination migration plugin manager.
    * @param \Drupal\migrate\Plugin\MigratePluginManager $idmap_plugin_manager
    *   The ID map migration plugin manager.
+   * @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
+   *   The migration field discovery service.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrateCckFieldPluginManagerInterface $cck_manager, MigrateFieldPluginManagerInterface $field_manager, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManager $source_plugin_manager, MigratePluginManager $process_plugin_manager, MigrateDestinationPluginManager $destination_plugin_manager, MigratePluginManager $idmap_plugin_manager) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManager $source_plugin_manager, MigratePluginManager $process_plugin_manager, MigrateDestinationPluginManager $destination_plugin_manager, MigratePluginManager $idmap_plugin_manager, FieldDiscoveryInterface $field_discovery) {
     parent::__construct($configuration, $plugin_id, $plugin_definition, $migration_plugin_manager, $source_plugin_manager, $process_plugin_manager, $destination_plugin_manager, $idmap_plugin_manager);
-    $this->cckPluginManager = $cck_manager;
-    $this->fieldPluginManager = $field_manager;
+    $this->fieldDiscovery = $field_discovery;
   }
 
   /**
@@ -110,13 +76,12 @@ public static function create(ContainerInterface $container, array $configuratio
       $configuration,
       $plugin_id,
       $plugin_definition,
-      $container->get('plugin.manager.migrate.cckfield'),
-      $container->get('plugin.manager.migrate.field'),
       $container->get('plugin.manager.migration'),
       $container->get('plugin.manager.migrate.source'),
       $container->get('plugin.manager.migrate.process'),
       $container->get('plugin.manager.migrate.destination'),
-      $container->get('plugin.manager.migrate.id_map')
+      $container->get('plugin.manager.migrate.id_map'),
+      $container->get('migrate_drupal.field_discovery')
     );
   }
 
@@ -126,44 +91,7 @@ public static function create(ContainerInterface $container, array $configuratio
   public function getProcess() {
     if (!$this->init) {
       $this->init = TRUE;
-      $source_plugin = $this->migrationPluginManager->createInstance($this->pluginId)->getSourcePlugin();
-      if ($source_plugin instanceof RequirementsInterface) {
-        try {
-          $source_plugin->checkRequirements();
-        }
-        catch (RequirementsException $e) {
-          // Kill the rest of the method.
-          $source_plugin = [];
-        }
-      }
-      foreach ($source_plugin as $row) {
-        $field_type = $row->getSourceProperty('type');
-
-        try {
-          $plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, [], $this);
-          $manager = $this->fieldPluginManager;
-        }
-        catch (PluginNotFoundException $ex) {
-          try {
-            $plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, [], $this);
-            $manager = $this->cckPluginManager;
-          }
-          catch (PluginNotFoundException $ex) {
-            continue;
-          }
-        }
-
-        if (!isset($this->processedFieldTypes[$field_type]) && $manager->hasDefinition($plugin_id)) {
-          $this->processedFieldTypes[$field_type] = TRUE;
-          // Allow the field plugin to alter the migration as necessary so that
-          // it knows how to handle fields of this type.
-          if (!isset($this->fieldPluginCache[$field_type])) {
-            $this->fieldPluginCache[$field_type] = $manager->createInstance($plugin_id, [], $this);
-          }
-        }
-        $method = $this->pluginDefinition[static::PLUGIN_METHOD];
-        call_user_func([$this->fieldPluginCache[$field_type], $method], $this);
-      }
+      $this->fieldDiscovery->addAllFieldProcesses($this);
     }
     return parent::getProcess();
   }
diff --git a/core/modules/migrate_drupal/tests/modules/field_discovery_test/field_discovery_test.info.yml b/core/modules/migrate_drupal/tests/modules/field_discovery_test/field_discovery_test.info.yml
new file mode 100644
index 000000000000..effa82da7752
--- /dev/null
+++ b/core/modules/migrate_drupal/tests/modules/field_discovery_test/field_discovery_test.info.yml
@@ -0,0 +1,6 @@
+name: 'Migrate drupal field discovery tet'
+type: module
+description: 'Module containing a test class exposing protected field discovery methods'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/migrate_drupal/tests/modules/field_discovery_test/src/FieldDiscoveryTestClass.php b/core/modules/migrate_drupal/tests/modules/field_discovery_test/src/FieldDiscoveryTestClass.php
new file mode 100644
index 000000000000..708f1d199062
--- /dev/null
+++ b/core/modules/migrate_drupal/tests/modules/field_discovery_test/src/FieldDiscoveryTestClass.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Drupal\field_discovery_test;
+
+use Drupal\Core\Logger\LoggerChannelInterface;
+use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
+use Drupal\migrate_drupal\FieldDiscovery;
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
+
+/**
+ * A test class to expose protected methods.
+ */
+class FieldDiscoveryTestClass extends FieldDiscovery {
+
+  /**
+   * An array of test data.
+   *
+   * @var array
+   */
+  protected $testData;
+
+  /**
+   * Constructs a FieldDiscoveryTestClass object.
+   *
+   * @param \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface $field_plugin_manager
+   *   The field plugin manager.
+   * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
+   *   The migration plugin manager.
+   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
+   *   The logger.
+   * @param array $test_data
+   *   An array of test data, keyed by method name, for overridden methods to
+   *   return for the purposes of testing other methods.
+   */
+  public function __construct(MigrateFieldPluginManagerInterface $field_plugin_manager, MigrationPluginManagerInterface $migration_plugin_manager, LoggerChannelInterface $logger, array $test_data = []) {
+    parent::__construct($field_plugin_manager, $migration_plugin_manager, $logger);
+    $this->testData = $test_data;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAllFields($core) {
+    if (!empty($this->testData['getAllFields'][$core])) {
+      return $this->testData['getAllFields'][$core];
+    }
+    return parent::getAllFields($core);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBundleFields($core, $entity_type_id, $bundle) {
+    return parent::getBundleFields($core, $entity_type_id, $bundle);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityFields($core, $entity_type_id) {
+    return parent::getEntityFields($core, $entity_type_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldInstanceStubMigrationDefinition($core) {
+    return parent::getFieldInstanceStubMigrationDefinition($core);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCoreVersion(MigrationInterface $migration) {
+    return parent::getCoreVersion($migration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldPlugin($field_type, MigrationInterface $migration) {
+    return parent::getFieldPlugin($field_type, $migration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourcePlugin($core) {
+    return parent::getSourcePlugin($core);
+  }
+
+}
diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d6/FieldDiscoveryTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d6/FieldDiscoveryTest.php
new file mode 100644
index 000000000000..cc2fb8c373cd
--- /dev/null
+++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/FieldDiscoveryTest.php
@@ -0,0 +1,304 @@
+<?php
+
+namespace Drupal\Tests\migrate_drupal\Kernel\d6;
+
+use Drupal\field\Plugin\migrate\source\d6\FieldInstance;
+use Drupal\field_discovery_test\FieldDiscoveryTestClass;
+use Drupal\migrate_drupal\FieldDiscoveryInterface;
+use Drupal\Tests\migrate_drupal\Traits\FieldDiscoveryTestTrait;
+
+/**
+ * Tests FieldDiscovery service against Drupal 6.
+ *
+ * @group migrate_drupal
+ * @coversDefaultClass \Drupal\migrate_drupal\FieldDiscovery
+ */
+class FieldDiscoveryTest extends MigrateDrupal6TestBase {
+
+  use FieldDiscoveryTestTrait;
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'menu_ui',
+    'comment',
+    'datetime',
+    'file',
+    'image',
+    'link',
+    'node',
+    'system',
+    'taxonomy',
+    'telephone',
+    'text',
+  ];
+
+  /**
+   * The Field discovery service.
+   *
+   * @var \Drupal\migrate_drupal\FieldDiscoveryInterface
+   */
+  protected $fieldDiscovery;
+
+  /**
+   * The field plugin manager.
+   *
+   * @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
+   */
+  protected $fieldPluginManager;
+
+  /**
+   * The migration plugin manager.
+   *
+   * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
+   */
+  protected $migrationPluginManager;
+  /**
+   * The logger.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelInterface
+   */
+  protected $logger;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->installConfig(['node']);
+    $this->executeMigration('d6_node_type');
+    $this->executeMigration('d6_field');
+    $this->executeMigration('d6_field_instance');
+    $this->fieldDiscovery = $this->container->get('migrate_drupal.field_discovery');
+    $this->migrationPluginManager = $this->container->get('plugin.manager.migration');
+    $this->fieldPluginManager = $this->container->get('plugin.manager.migrate.field');
+    $this->logger = $this->container->get('logger.channel.migrate_drupal');
+
+  }
+
+  /**
+   * Tests the addAllFieldProcesses method.
+   *
+   * @covers ::addAllFieldProcesses
+   */
+  public function testAddAllFieldProcesses() {
+    $expected_process_keys = [
+      'field_commander',
+      'field_company',
+      'field_company_2',
+      'field_company_3',
+      'field_sync',
+      'field_multivalue',
+      'field_test_text_single_checkbox',
+      'field_reference',
+      'field_reference_2',
+      'field_test',
+      'field_test_date',
+      'field_test_datestamp',
+      'field_test_datetime',
+      'field_test_decimal_radio_buttons',
+      'field_test_email',
+      'field_test_exclude_unset',
+      'field_test_filefield',
+      'field_test_float_single_checkbox',
+      'field_test_four',
+      'field_test_identical1',
+      'field_test_identical2',
+      'field_test_imagefield',
+      'field_test_integer_selectlist',
+      'field_test_link',
+      'field_test_phone',
+      'field_test_string_selectlist',
+      'field_test_text_single_checkbox2',
+      'field_test_three',
+      'field_test_two',
+    ];
+    $this->assertFieldProcessKeys($this->fieldDiscovery, $this->migrationPluginManager, FieldDiscoveryInterface::DRUPAL_6, $expected_process_keys);
+  }
+
+  /**
+   * Tests the addAllFieldProcesses method for field migrations.
+   *
+   * @covers ::addAllFieldProcesses
+   * @dataProvider addAllFieldProcessesAltersData
+   */
+  public function testAddAllFieldProcessesAlters($field_plugin_method, $expected_process) {
+    $this->assertFieldProcess($this->fieldDiscovery, $this->migrationPluginManager, FieldDiscoveryInterface::DRUPAL_6, $field_plugin_method, $expected_process);
+  }
+
+  /**
+   * Provides data for testAddAllFieldProcessesAlters.
+   *
+   * @return array
+   *   The data.
+   */
+  public function addAllFieldProcessesAltersData() {
+    return [
+      'Field Formatter' => [
+        'field_plugin_method' => 'alterFieldFormatterMigration',
+        'expected_process' => [
+          'options/type' => [
+            0 => [
+              'map' => [
+                'email' => [
+                  'email_formatter_default' => 'email_mailto',
+                  'email_formatter_contact' => 'basic_string',
+                  'email_formatter_plain' => 'basic_string',
+                  'email_formatter_spamspan' => 'basic_string',
+                  'email_default' => 'email_mailto',
+                  'email_contact' => 'basic_string',
+                  'email_plain' => 'basic_string',
+                  'email_spamspan' => 'basic_string',
+                ],
+                'text' => [
+                  'default' => 'text_default',
+                  'trimmed' => 'text_trimmed',
+                  'plain' => 'basic_string',
+                ],
+                'datetime' => [
+                  'date_default' => 'datetime_default',
+                ],
+                'filefield' => [
+                  'default' => 'file_default',
+                  'url_plain' => 'file_url_plain',
+                  'path_plain' => 'file_url_plain',
+                  'image_plain' => 'image',
+                  'image_nodelink' => 'image',
+                  'image_imagelink' => 'image',
+                ],
+                'link' => [
+                  'default' => 'link',
+                  'plain' => 'link',
+                  'absolute' => 'link',
+                  'title_plain' => 'link',
+                  'url' => 'link',
+                  'short' => 'link',
+                  'label' => 'link',
+                  'separate' => 'link_separate',
+                ],
+              ],
+            ],
+          ],
+        ],
+      ],
+      'Field Widget' => [
+        'field_plugin_method' => 'alterFieldWidgetMigration',
+        'expected_process' => [
+          'options/type' => [
+            'type' => [
+              'map' => [
+                'userreference' => 'userreference_default',
+                'nodereference' => 'nodereference_default',
+                'email_textfield' => 'email_default',
+                'text_textfield' => 'text_textfield',
+                'date' => 'datetime_default',
+                'datetime' => 'datetime_default',
+                'datestamp' => 'datetime_timestamp',
+                'filefield_widget' => 'file_generic',
+                'link' => 'link_default',
+              ],
+            ],
+          ],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Tests the addFields method.
+   *
+   * @covers ::addAllFieldProcesses
+   */
+  public function testAddFields() {
+    $this->migrateFields();
+    $field_discovery = $this->container->get('migrate_drupal.field_discovery');
+    $migration_plugin_manager = $this->container->get('plugin.manager.migration');
+    $definition = [
+      'migration_tags' => ['Drupal 6'],
+    ];
+    $migration = $migration_plugin_manager->createStubMigration($definition);
+    $field_discovery->addBundleFieldProcesses($migration, 'node', 'test_planet');
+    $actual_process = $migration->getProcess();
+    $expected_process = [
+      'field_multivalue' => [
+        0 => [
+          'plugin' => 'get',
+          'source' => 'field_multivalue',
+        ],
+      ],
+      'field_test_text_single_checkbox' => [
+        0 => [
+          'plugin' => 'sub_process',
+          'source' => 'field_test_text_single_checkbox',
+          'process' => [
+            'value' => 'value',
+            'format' => [
+              0 => [
+                'plugin' => 'static_map',
+                'bypass' => TRUE,
+                'source' => 'format',
+                'map' => [
+                  0 => NULL,
+                ],
+              ],
+              1 => [
+                'plugin' => 'skip_on_empty',
+                'method' => 'process',
+              ],
+              2 => [
+                'plugin' => 'migration',
+                'migration' => [
+                  0 => 'd6_filter_format',
+                  1 => 'd7_filter_format',
+                ],
+                'source' => 'format',
+              ],
+            ],
+          ],
+        ],
+      ],
+    ];
+    $this->assertEquals($expected_process, $actual_process);
+  }
+
+  /**
+   * Tests the getAllFields method.
+   *
+   * @covers ::getAllFields
+   */
+  public function testGetAllFields() {
+    $field_discovery_test = new FieldDiscoveryTestClass($this->fieldPluginManager, $this->migrationPluginManager, $this->logger);
+    $actual_fields = $field_discovery_test->getAllFields('6');
+    $this->assertSame(['node'], array_keys($actual_fields));
+    $this->assertSame(['employee', 'test_planet', 'page', 'story', 'test_page'], array_keys($actual_fields['node']));
+    $this->assertSame(21, count($actual_fields['node']['story']));
+    foreach ($actual_fields['node'] as $bundle => $fields) {
+      foreach ($fields as $field_name => $field_info) {
+        $this->assertArrayHasKey('type', $field_info);
+        $this->assertSame(22, count($field_info));
+        $this->assertEquals($bundle, $field_info['type_name']);
+      }
+    }
+  }
+
+  /**
+   * Tests the getSourcePlugin method.
+   *
+   * @covers ::getSourcePlugin
+   */
+  public function testGetSourcePlugin() {
+    $this->assertSourcePlugin('6', FieldInstance::class, [
+      'requirements_met' => TRUE,
+      'id' => 'd6_field_instance',
+      'source_module' => 'content',
+      'class' => 'Drupal\\field\\Plugin\\migrate\\source\\d6\\FieldInstance',
+      'provider' => [
+        0 => 'field',
+        1 => 'migrate_drupal',
+        2 => 'migrate',
+        4 => 'core',
+      ],
+    ]);
+  }
+
+}
diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php
new file mode 100644
index 000000000000..a36a994e71e5
--- /dev/null
+++ b/core/modules/migrate_drupal/tests/src/Kernel/d7/FieldDiscoveryTest.php
@@ -0,0 +1,364 @@
+<?php
+
+namespace Drupal\Tests\migrate_drupal\Kernel\d7;
+
+use Drupal\comment\Entity\CommentType;
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\field\Plugin\migrate\source\d7\FieldInstance;
+use Drupal\migrate_drupal\FieldDiscovery;
+use Drupal\migrate_drupal\FieldDiscoveryInterface;
+use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
+use Drupal\node\Entity\NodeType;
+use Drupal\taxonomy\Entity\Vocabulary;
+use Drupal\Tests\migrate_drupal\Traits\FieldDiscoveryTestTrait;
+use Drupal\field_discovery_test\FieldDiscoveryTestClass;
+
+/**
+ * Test FieldDiscovery Service against Drupal 7.
+ *
+ * @group migrate_drupal
+ * @coversDefaultClass \Drupal\migrate_drupal\FieldDiscovery
+ */
+class FieldDiscoveryTest extends MigrateDrupal7TestBase {
+
+  use FieldDiscoveryTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'comment',
+    'datetime',
+    'file',
+    'image',
+    'link',
+    'node',
+    'system',
+    'taxonomy',
+    'telephone',
+    'text',
+  ];
+
+  /**
+   * The Field discovery service.
+   *
+   * @var \Drupal\migrate_drupal\FieldDiscoveryInterface
+   */
+  protected $fieldDiscovery;
+
+  /**
+   * The field plugin manager.
+   *
+   * @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
+   */
+  protected $fieldPluginManager;
+
+  /**
+   * The migration plugin manager.
+   *
+   * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
+   */
+  protected $migrationPluginManager;
+
+  /**
+   * The logger.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelInterface
+   */
+  protected $logger;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->installConfig(static::$modules);
+    $node_types = [
+      'page' => 'comment_node_page',
+      'article' => 'comment_node_article',
+      'blog' => 'comment_node_blog',
+      'book' => 'comment_node_book',
+      'forum' => 'comment_forum',
+      'test_content_type' => 'comment_node_test_content_type',
+    ];
+    foreach ($node_types as $node_type => $comment_type) {
+      NodeType::create([
+        'type' => $node_type,
+        'label' => $this->randomString(),
+      ])->save();
+
+      CommentType::create([
+        'id' => $comment_type,
+        'label' => $this->randomString(),
+        'target_entity_type_id' => 'node',
+      ])->save();
+    }
+
+    Vocabulary::create(['vid' => 'test_vocabulary'])->save();
+    $this->executeMigrations(['d7_field', 'd7_field_instance']);
+
+    $this->fieldDiscovery = $this->container->get('migrate_drupal.field_discovery');
+    $this->migrationPluginManager = $this->container->get('plugin.manager.migration');
+    $this->fieldPluginManager = $this->container->get('plugin.manager.migrate.field');
+    $this->logger = $this->container->get('logger.channel.migrate_drupal');
+  }
+
+  /**
+   * Tests the addAllFieldProcesses method.
+   *
+   * @covers ::addAllFieldProcesses
+   */
+  public function testAddAllFieldProcesses() {
+    $expected_process_keys = [
+      'comment_body',
+      'field_integer',
+      'body',
+      'field_text_plain',
+      'field_text_filtered',
+      'field_text_plain_filtered',
+      'field_text_long_plain',
+      'field_text_long_filtered',
+      'field_text_long_plain_filtered',
+      'field_text_sum_plain',
+      'field_text_sum_filtered',
+      'field_text_sum_plain_filtered',
+      'field_tags',
+      'field_image',
+      'field_link',
+      'field_reference',
+      'field_reference_2',
+      'taxonomy_forums',
+      'field_boolean',
+      'field_email',
+      'field_phone',
+      'field_date',
+      'field_date_with_end_time',
+      'field_file',
+      'field_float',
+      'field_images',
+      'field_text_list',
+      'field_integer_list',
+      'field_long_text',
+      'field_term_reference',
+      'field_text',
+      'field_node_entityreference',
+      'field_user_entityreference',
+      'field_term_entityreference',
+      'field_private_file',
+      'field_datetime_without_time',
+      'field_date_without_time',
+      'field_float_list',
+    ];
+    $this->assertFieldProcessKeys($this->fieldDiscovery, $this->migrationPluginManager, '7', $expected_process_keys);
+  }
+
+  /**
+   * Tests the addAllFieldProcesses method for field migrations.
+   *
+   * @covers ::addAllFieldProcesses
+   * @dataProvider addAllFieldProcessesAltersData
+   */
+  public function testAddAllFieldProcessesAlters($field_plugin_method, $expected_process) {
+    $this->assertFieldProcess($this->fieldDiscovery, $this->migrationPluginManager, FieldDiscoveryInterface::DRUPAL_7, $field_plugin_method, $expected_process);
+  }
+
+  /**
+   * Provides data for testAddAllFieldProcessesAlters.
+   *
+   * @return array
+   *   The data.
+   */
+  public function addAllFieldProcessesAltersData() {
+    return [
+      'Field Instance' => [
+        'field_plugin_method' => 'alterFieldInstanceMigration',
+        'expected_process' => [
+          'settings/title' => [
+            0 => [
+              'plugin' => 'static_map',
+              'source' => 'settings/title',
+              'bypass' => TRUE,
+              'map' => [
+                'disabled' => 0,
+                'optional' => 1,
+                'required' => 2,
+              ],
+            ],
+          ],
+        ],
+      ],
+      'Field Formatter' => [
+        'field_plugin_method' => 'alterFieldFormatterMigration',
+        'expected_process' => [
+          'options/type' => [
+            0 => [
+              'map' => [
+                'taxonomy_term_reference' => [
+                  'taxonomy_term_reference_link' => 'entity_reference_label',
+                ],
+                'link_field' => [
+                  'link_default' => 'link',
+                ],
+                'entityreference' => [
+                  'entityreference_label' => 'entity_reference_label',
+                  'entityreference_entity_id' => 'entity_reference_entity_id',
+                  'entityreference_entity_view' => 'entity_reference_entity_view',
+                ],
+                'email' => [
+                  'email_formatter_default' => 'email_mailto',
+                  'email_formatter_contact' => 'basic_string',
+                  'email_formatter_plain' => 'basic_string',
+                  'email_formatter_spamspan' => 'basic_string',
+                  'email_default' => 'email_mailto',
+                  'email_contact' => 'basic_string',
+                  'email_plain' => 'basic_string',
+                  'email_spamspan' => 'basic_string',
+                ],
+                'phone' => [
+                  'phone' => 'basic_string',
+                ],
+                'datetime' => [
+                  'date_default' => 'datetime_default',
+                ],
+                'file' => [
+                  'default' => 'file_default',
+                  'url_plain' => 'file_url_plain',
+                  'path_plain' => 'file_url_plain',
+                  'image_plain' => 'image',
+                  'image_nodelink' => 'image',
+                  'image_imagelink' => 'image',
+                ],
+              ],
+            ],
+          ],
+        ],
+      ],
+      'Field Widget' => [
+        'field_plugin_method' => 'alterFieldWidgetMigration',
+        'expected_process' => [
+          'options/type' => [
+            'type' => [
+              'map' => [
+                'd7_text' => 'd7_text_default',
+                'number_default' => 'number_default_default',
+                'taxonomy_term_reference' => 'taxonomy_term_reference_default',
+                'image' => 'image_default',
+                'link_field' => 'link_default',
+                'entityreference' => 'entityreference_default',
+                'list' => 'list_default',
+                'email_textfield' => 'email_default',
+                'phone' => 'phone_default',
+                'date' => 'datetime_default',
+                'datetime' => 'datetime_default',
+                'datestamp' => 'datetime_timestamp',
+                'filefield_widget' => 'file_generic',
+              ],
+            ],
+          ],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Tests the getAllFields method.
+   *
+   * @covers ::getAllFields
+   */
+  public function testGetAllFields() {
+    $field_discovery_test = new FieldDiscoveryTestClass($this->fieldPluginManager, $this->migrationPluginManager, $this->logger);
+    $actual_fields = $field_discovery_test->getAllFields('7');
+    $this->assertSame(['comment', 'node', 'user', 'taxonomy_term'], array_keys($actual_fields));
+    $this->assertArrayHasKey('test_vocabulary', $actual_fields['taxonomy_term']);
+    $this->assertArrayHasKey('user', $actual_fields['user']);
+    $this->assertArrayHasKey('test_content_type', $actual_fields['node']);
+    $this->assertSame(6, count($actual_fields['node']));
+    $this->assertSame(6, count($actual_fields['comment']));
+    $this->assertSame(22, count($actual_fields['node']['test_content_type']));
+    foreach ($actual_fields as $entity_type_id => $bundles) {
+      foreach ($bundles as $bundle => $fields) {
+        foreach ($fields as $field_name => $field_info) {
+          $this->assertArrayHasKey('field_definition', $field_info);
+          $this->assertEquals($entity_type_id, $field_info['entity_type']);
+          $this->assertEquals($bundle, $field_info['bundle']);
+        }
+      }
+    }
+  }
+
+  /**
+   * Tests the getSourcePlugin method.
+   *
+   * @covers ::getSourcePlugin
+   */
+  public function testGetSourcePlugin() {
+    $this->assertSourcePlugin('7', FieldInstance::class, [
+      'requirements_met' => TRUE,
+      'id' => 'd7_field_instance',
+      'source_module' => 'field',
+      'class' => 'Drupal\\field\\Plugin\\migrate\\source\\d7\\FieldInstance',
+      'provider' => [
+        0 => 'field',
+        1 => 'migrate_drupal',
+        2 => 'migrate',
+        4 => 'core',
+      ],
+    ]);
+  }
+
+  /**
+   * Tests the fallback to deprecated CCK Plugin Manager.
+   *
+   * @covers ::getCckPluginManager
+   * @group legacy
+   * @expectedDeprecation TextField is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\text\Plugin\migrate\field\d6\TextField or \Drupal\text\Plugin\migrate\field\d7\TextField instead.
+   * @expectedDeprecation CckFieldPluginBase is deprecated in Drupal 8.3.x and will be be removed before Drupal 9.0.x. Use \Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase instead.
+   * @expectedDeprecation MigrateCckFieldInterface is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\migrate_drupal\Annotation\MigrateField instead.
+   */
+  public function testGetCckPluginManager() {
+    $definition = [
+      'migration_tags' => ['Drupal 7'],
+    ];
+    $migration = $this->migrationPluginManager->createStubMigration($definition);
+    $field_plugin_manager = $this->prophesize(MigrateFieldPluginManagerInterface::class);
+    $field_plugin_manager->getPluginIdFromFieldType('text_long', ['core' => '7'], $migration)->willThrow(PluginNotFoundException::class);
+    $field_discovery = new FieldDiscovery($field_plugin_manager->reveal(), $this->migrationPluginManager, $this->logger);
+    $field_discovery->addBundleFieldProcesses($migration, 'comment', 'comment_node_page');
+    $actual_process = $migration->getProcess();
+    $expected_process = [
+      'comment_body' => [
+        0 => [
+          'plugin' => 'sub_process',
+          'source' => 'comment_body',
+          'process' => [
+            'value' => 'value',
+            'format' => [
+              0 => [
+                'plugin' => 'static_map',
+                'bypass' => TRUE,
+                'source' => 'format',
+                'map' => [
+                  0 => NULL,
+                ],
+              ],
+              1 => [
+                'plugin' => 'skip_on_empty',
+                'method' => 'process',
+              ],
+              2 => [
+                'plugin' => 'migration',
+                'migration' => [
+                  0 => 'd6_filter_format',
+                  1 => 'd7_filter_format',
+                ],
+                'source' => 'format',
+              ],
+            ],
+          ],
+        ],
+      ],
+    ];
+    $this->assertEquals($expected_process, $actual_process);
+  }
+
+}
diff --git a/core/modules/migrate_drupal/tests/src/Traits/FieldDiscoveryTestTrait.php b/core/modules/migrate_drupal/tests/src/Traits/FieldDiscoveryTestTrait.php
new file mode 100644
index 000000000000..34a16ad85ea7
--- /dev/null
+++ b/core/modules/migrate_drupal/tests/src/Traits/FieldDiscoveryTestTrait.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Drupal\Tests\migrate_drupal\Traits;
+
+use Drupal\field_discovery_test\FieldDiscoveryTestClass;
+use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
+use Drupal\migrate_drupal\FieldDiscoveryInterface;
+
+/**
+ * Helper functions to test field discovery.
+ */
+trait FieldDiscoveryTestTrait {
+
+  /**
+   * Asserts the field discovery returns the expected processes.
+   *
+   * @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
+   *   The Field Discovery service.
+   * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
+   *   The migration plugin manager service.
+   * @param string $core
+   *   The Drupal core version, either '6', or '7'.
+   * @param string $field_plugin_method
+   *   (optional) The field plugin method to use.
+   * @param array $expected_process
+   *   (optional) The expected resulting process.
+   * @param string $entity_type_id
+   *   (optional) The entity type id.
+   * @param string $bundle
+   *   (optional) The bundle.
+   */
+  public function assertFieldProcess(FieldDiscoveryInterface $field_discovery, MigrationPluginManagerInterface $migration_plugin_manager, $core, $field_plugin_method = NULL, array $expected_process = [], $entity_type_id = NULL, $bundle = NULL) {
+    $definition = [
+      'migration_tags' => ['Drupal ' . $core],
+      'field_plugin_method' => $field_plugin_method,
+    ];
+    $migration = $migration_plugin_manager->createStubMigration($definition);
+    if ($bundle) {
+      $field_discovery->addBundleFieldProcesses($migration, $entity_type_id, $bundle);
+    }
+    elseif ($entity_type_id) {
+      $field_discovery->addEntityFieldProcesses($migration, $entity_type_id);
+    }
+    else {
+      $field_discovery->addAllFieldProcesses($migration);
+    }
+    $actual_process = $migration->getProcess();
+    $this->assertSame($expected_process, $actual_process);
+  }
+
+  /**
+   * Asserts the field discovery returns the expected processes.
+   *
+   * @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
+   *   The Field Discovery service.
+   * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
+   *   The migration plugin manager service.
+   * @param string $core
+   *   The Drupal core version, either '6', or '7'.
+   * @param array $expected_process_keys
+   *   (optional) The expected resulting process_keys.
+   * @param string $entity_type_id
+   *   (optional) The entity type id.
+   * @param string $bundle
+   *   (optional) The bundle.
+   */
+  public function assertFieldProcessKeys(FieldDiscoveryInterface $field_discovery, MigrationPluginManagerInterface $migration_plugin_manager, $core, array $expected_process_keys, $entity_type_id = NULL, $bundle = NULL) {
+    $definition = [
+      'migration_tags' => ['Drupal ' . $core],
+    ];
+    $migration = $migration_plugin_manager->createStubMigration($definition);
+    if ($bundle) {
+      $field_discovery->addBundleFieldProcesses($migration, $entity_type_id, $bundle);
+    }
+    elseif ($entity_type_id) {
+      $field_discovery->addEntityFieldProcesses($migration, $entity_type_id);
+    }
+    else {
+      $field_discovery->addAllFieldProcesses($migration);
+    }
+    $actual_process = $migration->getProcess();
+    $actual = array_keys($actual_process);
+    $this->assertSame(sort($expected_process_keys), sort($actual));
+  }
+
+  /**
+   * Asserts a migrate source plugin.
+   *
+   * @param string $core
+   *   The Drupal core version.
+   * @param string $class
+   *   The expected class of the source plugin.
+   * @param array $expected_definition
+   *   The expected source plugin definition.
+   */
+  public function assertSourcePlugin($core, $class, array $expected_definition) {
+    $field_discovery = new FieldDiscoveryTestClass($this->fieldPluginManager, $this->migrationPluginManager, $this->logger);
+    $source = $field_discovery->getSourcePlugin($core);
+    $this->assertInstanceOf($class, $source);
+    $this->assertSame($expected_definition, $source->getPluginDefinition());
+  }
+
+}
diff --git a/core/modules/migrate_drupal/tests/src/Unit/FieldDiscoveryTest.php b/core/modules/migrate_drupal/tests/src/Unit/FieldDiscoveryTest.php
new file mode 100644
index 000000000000..852d4fa31961
--- /dev/null
+++ b/core/modules/migrate_drupal/tests/src/Unit/FieldDiscoveryTest.php
@@ -0,0 +1,358 @@
+<?php
+
+namespace Drupal\Tests\migrate_drupal\Unit;
+
+use Drupal\Core\Logger\LoggerChannelInterface;
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
+use Drupal\field_discovery_test\FieldDiscoveryTestClass;
+use Drupal\migrate_drupal\FieldDiscoveryInterface;
+use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the FieldDiscovery Class.
+ *
+ * @group migrate_drupal
+ * @coversDefaultClass \Drupal\migrate_drupal\FieldDiscovery
+ */
+class FieldDiscoveryTest extends UnitTestCase {
+
+  /**
+   * A MigrateFieldPluginManager prophecy.
+   *
+   * @var \Prophecy\Prophecy\ObjectProphecy
+   */
+  protected $fieldPluginManager;
+
+  /**
+   * A MigrationPluginManager prophecy.
+   *
+   * @var \Prophecy\Prophecy\ObjectProphecy
+   */
+  protected $migrationPluginManager;
+
+  /**
+   * A LoggerChannelInterface prophecy.
+   *
+   * @var \Prophecy\Prophecy\ObjectProphecy
+   */
+  protected $logger;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->fieldPluginManager = $this->prophesize(MigrateFieldPluginManagerInterface::class);
+    $this->migrationPluginManager = $this->prophesize(MigrationPluginManagerInterface::class);
+    $this->logger = $this->prophesize(LoggerChannelInterface::class);
+  }
+
+  /**
+   * Tests the protected getEntityFields method.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   * @param array $expected_fields
+   *   The expected fields.
+   *
+   * @covers ::getEntityFields
+   * @dataProvider getEntityFieldsData
+   */
+  public function testGetEntityFields($entity_type_id, array $expected_fields) {
+    $test_data = [
+      'getAllFields' => [
+        '7' => $this->getAllFieldData(),
+      ],
+    ];
+    $field_discovery = new FieldDiscoveryTestClass($this->fieldPluginManager->reveal(), $this->migrationPluginManager->reveal(), $this->logger->reveal(), $test_data);
+    $actual_fields = $field_discovery->getEntityFields('7', $entity_type_id);
+    $this->assertSame($expected_fields, $actual_fields);
+  }
+
+  /**
+   * Provides data for testGetEntityFields.
+   *
+   * @return array
+   *   The data.
+   */
+  public function getEntityFieldsData() {
+    return [
+      'Node' => [
+        'entity_type_id' => 'node',
+        'expected_fields' => [
+          'content_type_1' => [
+            'field_1' => ['field_info_key' => 'field_1_data'],
+            'field_2' => ['field_info_key' => 'field_2_data'],
+            'field_3' => ['field_info_key' => 'field_3_data'],
+          ],
+          'content_type_2' => [
+            'field_1' => ['field_info_key' => 'field_1_data'],
+            'field_4' => ['field_info_key' => 'field_4_data'],
+            'field_5' => ['field_info_key' => 'field_5_data'],
+          ],
+        ],
+      ],
+      'User' => [
+        'entity_type_id' => 'user',
+        'expected_fields' => [
+          'user' => [
+            'user_field_1' => ['field_info_key' => 'user_field_1_data'],
+          ],
+        ],
+      ],
+      'Comment' => [
+        'entity_type_id' => 'comment',
+        'expected_fields' => [
+          'comment_node_content_type_1' => [
+            'cfield_1' => ['field_info_key' => 'field_1_data'],
+            'cfield_2' => ['field_info_key' => 'field_2_data'],
+            'cfield_3' => ['field_info_key' => 'field_3_data'],
+          ],
+          'comment_node_content_type_2' => [
+            'cfield_1' => ['field_info_key' => 'field_1_data'],
+            'cfield_4' => ['field_info_key' => 'field_4_data'],
+            'cfield_5' => ['field_info_key' => 'field_5_data'],
+          ],
+        ],
+      ],
+      'Non-existent Entity' => [
+        'entity_type_id' => 'custom_entity',
+        'expected_fields' => [],
+      ],
+    ];
+  }
+
+  /**
+   * Tests the protected getEntityFields method.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   * @param string $bundle
+   *   The bundle.
+   * @param array $expected_fields
+   *   The expected fields.
+   *
+   * @covers ::getBundleFields
+   * @dataProvider getBundleFieldsData
+   */
+  public function testGetBundleFields($entity_type_id, $bundle, array $expected_fields) {
+    $test_data = [
+      'getAllFields' => [
+        '7' => $this->getAllFieldData(),
+      ],
+    ];
+    $field_discovery = new FieldDiscoveryTestClass($this->fieldPluginManager->reveal(), $this->migrationPluginManager->reveal(), $this->logger->reveal(), $test_data);
+    $actual_fields = $field_discovery->getBundleFields('7', $entity_type_id, $bundle);
+    $this->assertSame($expected_fields, $actual_fields);
+  }
+
+  /**
+   * Provides data for testGetBundleFields.
+   *
+   * @return array
+   *   The data.
+   */
+  public function getBundleFieldsData() {
+    return [
+      'Node - Content Type 1' => [
+        'entity_type_id' => 'node',
+        'bundle' => 'content_type_1',
+        'expected_fields' => [
+          'field_1' => ['field_info_key' => 'field_1_data'],
+          'field_2' => ['field_info_key' => 'field_2_data'],
+          'field_3' => ['field_info_key' => 'field_3_data'],
+        ],
+      ],
+      'Node - Content Type 2' => [
+        'entity_type_id' => 'node',
+        'bundle' => 'content_type_2',
+        'expected_fields' => [
+          'field_1' => ['field_info_key' => 'field_1_data'],
+          'field_4' => ['field_info_key' => 'field_4_data'],
+          'field_5' => ['field_info_key' => 'field_5_data'],
+        ],
+      ],
+      'User' => [
+        'entity_type_id' => 'user',
+        'bundle' => 'user',
+        'expected_fields' => [
+            'user_field_1' => ['field_info_key' => 'user_field_1_data'],
+        ],
+      ],
+      'Comment - Content Type 1' => [
+        'entity_type_id' => 'comment',
+        'bundle' => 'comment_node_content_type_1',
+        'expected_fields' => [
+          'cfield_1' => ['field_info_key' => 'field_1_data'],
+          'cfield_2' => ['field_info_key' => 'field_2_data'],
+          'cfield_3' => ['field_info_key' => 'field_3_data'],
+        ],
+      ],
+      'Comment - Content Type 2' => [
+        'entity_type_id' => 'comment',
+        'bundle' => 'comment_node_content_type_2',
+        'expected_fields' => [
+          'cfield_1' => ['field_info_key' => 'field_1_data'],
+          'cfield_4' => ['field_info_key' => 'field_4_data'],
+          'cfield_5' => ['field_info_key' => 'field_5_data'],
+        ],
+      ],
+      'Non-existent Entity Type' => [
+        'entity_type_id' => 'custom_entity',
+        'bundle' => 'content_type_1',
+        'expected_fields' => [],
+      ],
+      'Non-existent Bundle' => [
+        'entity_type_id' => 'node',
+        'bundle' => 'content_type_3',
+        'expected_fields' => [],
+      ],
+    ];
+  }
+
+  /**
+   * Test the protected getCoreVersion method.
+   *
+   * @param string[] $tags
+   *   The migration tags.
+   * @param string|bool $expected_result
+   *   The expected return value of the method.
+   *
+   * @covers ::getCoreVersion
+   * @dataProvider getCoreVersionData
+   */
+  public function testGetCoreVersion(array $tags, $expected_result) {
+    $migration = $this->prophesize(MigrationInterface::class);
+    $migration->getMigrationTags()->willReturn($tags);
+    $field_discovery = new FieldDiscoveryTestClass($this->fieldPluginManager->reveal(), $this->migrationPluginManager->reveal(), $this->logger->reveal());
+    if (!$expected_result) {
+      $this->setExpectedException(\InvalidArgumentException::class);
+    }
+    $actual_result = $field_discovery->getCoreVersion($migration->reveal());
+    $this->assertEquals($expected_result, $actual_result);
+  }
+
+  /**
+   * Provides data for testGetCoreVersion()
+   *
+   * @return array
+   *   The test data.
+   */
+  public function getCoreVersionData() {
+    return [
+      'Drupal 7' => [
+        'tags' => ['Drupal 7'],
+        'result' => '7',
+      ],
+      'Drupal 6' => [
+        'tags' => ['Drupal 6'],
+        'result' => '6',
+      ],
+      'D7 with others' => [
+        'tags' => ['Drupal 7', 'Translation', 'Other Tag'],
+        'result' => '7',
+      ],
+      'Both (d7 has priority)' => [
+        'tags' => ['Drupal 6', 'Drupal 7'],
+        'result' => '7',
+      ],
+      'Neither' => [
+        'tags' => ['drupal 6', 'Drupal_6', 'This contains Drupal 7 but is not'],
+        'result' => FALSE,
+      ],
+    ];
+  }
+
+  /**
+   * Returns dummy data to test the field getters.
+   */
+  protected function getAllFieldData() {
+    return [
+      'node' => [
+        'content_type_1' => [
+          'field_1' => ['field_info_key' => 'field_1_data'],
+          'field_2' => ['field_info_key' => 'field_2_data'],
+          'field_3' => ['field_info_key' => 'field_3_data'],
+        ],
+        'content_type_2' => [
+          'field_1' => ['field_info_key' => 'field_1_data'],
+          'field_4' => ['field_info_key' => 'field_4_data'],
+          'field_5' => ['field_info_key' => 'field_5_data'],
+        ],
+      ],
+      'user' => [
+        'user' => [
+          'user_field_1' => ['field_info_key' => 'user_field_1_data'],
+        ],
+      ],
+      'comment' => [
+        'comment_node_content_type_1' => [
+          'cfield_1' => ['field_info_key' => 'field_1_data'],
+          'cfield_2' => ['field_info_key' => 'field_2_data'],
+          'cfield_3' => ['field_info_key' => 'field_3_data'],
+        ],
+        'comment_node_content_type_2' => [
+          'cfield_1' => ['field_info_key' => 'field_1_data'],
+          'cfield_4' => ['field_info_key' => 'field_4_data'],
+          'cfield_5' => ['field_info_key' => 'field_5_data'],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Tests the getFieldInstanceStubMigration method.
+   *
+   * @param mixed $core
+   *   The Drupal core version.
+   * @param array|bool $expected_definition
+   *   The expected migration definition, or false if an exception is expected.
+   *
+   * @covers ::getFieldInstanceStubMigrationDefinition
+   * @dataProvider getFieldInstanceStubMigrationDefinition
+   */
+  public function testGetFieldInstanceStubMigrationDefinition($core, $expected_definition) {
+    $field_discovery = new FieldDiscoveryTestClass($this->fieldPluginManager->reveal(), $this->migrationPluginManager->reveal(), $this->logger->reveal());
+    if (!$expected_definition) {
+      $this->setExpectedException(\InvalidArgumentException::class, sprintf("Drupal version %s is not supported. Valid values for Drupal core version are '6' and '7'.", $core));
+    }
+    $actual_definition = $field_discovery->getFieldInstanceStubMigrationDefinition($core);
+    $this->assertSame($expected_definition, $actual_definition);
+  }
+
+  /**
+   * Provides data for testGetFieldInstanceStubMigrationDefinition.
+   *
+   * @return array
+   *   The data.
+   */
+  public function getFieldInstanceStubMigrationDefinition() {
+    return [
+      'Drupal 6' => [
+        'core' => FieldDiscoveryInterface::DRUPAL_6,
+        'expected_definition' => [
+          'destination' => ['plugin' => 'null'],
+          'idMap' => ['plugin' => 'null'],
+          'source' => [
+            'ignore_map' => TRUE,
+            'plugin' => 'd6_field_instance',
+          ],
+        ],
+      ],
+      'Drupal 7' => [
+        'core' => FieldDiscoveryInterface::DRUPAL_7,
+        'expected_definition' => [
+          'destination' => ['plugin' => 'null'],
+          'idMap' => ['plugin' => 'null'],
+          'source' => [
+            'ignore_map' => TRUE,
+            'plugin' => 'd7_field_instance',
+          ],
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php b/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php
index 94c759bea45b..a8fa1d0bbe1b 100644
--- a/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php
+++ b/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php
@@ -3,13 +3,11 @@
 namespace Drupal\node\Plugin\migrate;
 
 use Drupal\Component\Plugin\Derivative\DeriverBase;
-use Drupal\Component\Plugin\Exception\PluginNotFoundException;
 use Drupal\Core\Database\DatabaseExceptionWrapper;
 use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
 use Drupal\migrate\Exception\RequirementsException;
 use Drupal\migrate\Plugin\MigrationDeriverTrait;
-use Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface;
-use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
+use Drupal\migrate_drupal\FieldDiscoveryInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -26,57 +24,33 @@ class D6NodeDeriver extends DeriverBase implements ContainerDeriverInterface {
   protected $basePluginId;
 
   /**
-   * Already-instantiated cckfield plugins, keyed by ID.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldInterface[]
-   */
-  protected $cckPluginCache;
-
-  /**
-   * The CCK plugin manager.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface
-   */
-  protected $cckPluginManager;
-
-  /**
-   * Already-instantiated field plugins, keyed by ID.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateFieldInterface[]
-   */
-  protected $fieldPluginCache;
-
-  /**
-   * The field plugin manager.
+   * Whether or not to include translations.
    *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
+   * @var bool
    */
-  protected $fieldPluginManager;
+  protected $includeTranslations;
 
   /**
-   * Whether or not to include translations.
+   * The migration field discovery service.
    *
-   * @var bool
+   * @var \Drupal\migrate_drupal\FieldDiscoveryInterface
    */
-  protected $includeTranslations;
+  protected $fieldDiscovery;
 
   /**
    * D6NodeDeriver constructor.
    *
    * @param string $base_plugin_id
    *   The base plugin ID for the plugin ID.
-   * @param \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface $cck_manager
-   *   The CCK plugin manager.
-   * @param \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface $field_manager
-   *   The field plugin manager.
    * @param bool $translations
    *   Whether or not to include translations.
+   * @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
+   *   The migration field discovery service.
    */
-  public function __construct($base_plugin_id, MigrateCckFieldPluginManagerInterface $cck_manager, MigrateFieldPluginManagerInterface $field_manager, $translations) {
+  public function __construct($base_plugin_id, $translations, FieldDiscoveryInterface $field_discovery) {
     $this->basePluginId = $base_plugin_id;
-    $this->cckPluginManager = $cck_manager;
-    $this->fieldPluginManager = $field_manager;
     $this->includeTranslations = $translations;
+    $this->fieldDiscovery = $field_discovery;
   }
 
   /**
@@ -86,22 +60,13 @@ public static function create(ContainerInterface $container, $base_plugin_id) {
     // Translations don't make sense unless we have content_translation.
     return new static(
       $base_plugin_id,
-      $container->get('plugin.manager.migrate.cckfield'),
-      $container->get('plugin.manager.migrate.field'),
-      $container->get('module_handler')->moduleExists('content_translation')
+      $container->get('module_handler')->moduleExists('content_translation'),
+      $container->get('migrate_drupal.field_discovery')
     );
   }
 
   /**
-   * Gets the definition of all derivatives of a base plugin.
-   *
-   * @param array $base_plugin_definition
-   *   The definition array of the base plugin.
-   *
-   * @return array
-   *   An array of full derivative definitions keyed on derivative id.
-   *
-   * @see \Drupal\Component\Plugin\Derivative\DeriverBase::getDerivativeDefinition()
+   * {@inheritdoc}
    */
   public function getDerivativeDefinitions($base_plugin_definition) {
     if ($base_plugin_definition['id'] == 'd6_node_translation' && !$this->includeTranslations) {
@@ -119,22 +84,6 @@ public function getDerivativeDefinitions($base_plugin_definition) {
       return $this->derivatives;
     }
 
-    // Read all field instance definitions in the source database.
-    $fields = [];
-    try {
-      $source_plugin = static::getSourcePlugin('d6_field_instance');
-      $source_plugin->checkRequirements();
-
-      foreach ($source_plugin as $row) {
-        $fields[$row->getSourceProperty('type_name')][$row->getSourceProperty('field_name')] = $row->getSource();
-      }
-    }
-    catch (RequirementsException $e) {
-      // If checkRequirements() failed then the content module did not exist and
-      // we do not have any fields. Therefore, $fields will be empty and
-      // below we'll create a migration just for the node properties.
-    }
-
     try {
       foreach ($node_types as $row) {
         $node_type = $row->getSourceProperty('type');
@@ -156,32 +105,7 @@ public function getDerivativeDefinitions($base_plugin_definition) {
 
         /** @var \Drupal\migrate\Plugin\Migration $migration */
         $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($values);
-        if (isset($fields[$node_type])) {
-          foreach ($fields[$node_type] as $field_name => $info) {
-            $field_type = $info['type'];
-            try {
-              $plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, ['core' => 6], $migration);
-              if (!isset($this->fieldPluginCache[$field_type])) {
-                $this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($plugin_id, ['core' => 6], $migration);
-              }
-              $this->fieldPluginCache[$field_type]
-                ->defineValueProcessPipeline($migration, $field_name, $info);
-            }
-            catch (PluginNotFoundException $ex) {
-              try {
-                $plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, ['core' => 6], $migration);
-                if (!isset($this->cckPluginCache[$field_type])) {
-                  $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($plugin_id, ['core' => 6], $migration);
-                }
-                $this->cckPluginCache[$field_type]
-                  ->processCckFieldValues($migration, $field_name, $info);
-              }
-              catch (PluginNotFoundException $ex) {
-                $migration->setProcessOfProperty($field_name, $field_name);
-              }
-            }
-          }
-        }
+        $this->fieldDiscovery->addBundleFieldProcesses($migration, 'node', $node_type);
         $this->derivatives[$node_type] = $migration->getPluginDefinition();
       }
     }
diff --git a/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php b/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php
index 25099cf178db..380aab81e4ef 100644
--- a/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php
+++ b/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php
@@ -3,13 +3,11 @@
 namespace Drupal\node\Plugin\migrate;
 
 use Drupal\Component\Plugin\Derivative\DeriverBase;
-use Drupal\Component\Plugin\Exception\PluginNotFoundException;
 use Drupal\Core\Database\DatabaseExceptionWrapper;
 use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
 use Drupal\migrate\Exception\RequirementsException;
 use Drupal\migrate\Plugin\MigrationDeriverTrait;
-use Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface;
-use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
+use Drupal\migrate_drupal\FieldDiscoveryInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -26,57 +24,33 @@ class D7NodeDeriver extends DeriverBase implements ContainerDeriverInterface {
   protected $basePluginId;
 
   /**
-   * Already-instantiated cckfield plugins, keyed by ID.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldInterface[]
-   */
-  protected $cckPluginCache;
-
-  /**
-   * The CCK plugin manager.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface
-   */
-  protected $cckPluginManager;
-
-  /**
-   * Already-instantiated field plugins, keyed by ID.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateFieldInterface[]
-   */
-  protected $fieldPluginCache;
-
-  /**
-   * The field plugin manager.
+   * Whether or not to include translations.
    *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
+   * @var bool
    */
-  protected $fieldPluginManager;
+  protected $includeTranslations;
 
   /**
-   * Whether or not to include translations.
+   * The migration field discovery service.
    *
-   * @var bool
+   * @var \Drupal\migrate_drupal\FieldDiscoveryInterface
    */
-  protected $includeTranslations;
+  protected $fieldDiscovery;
 
   /**
    * D7NodeDeriver constructor.
    *
    * @param string $base_plugin_id
    *   The base plugin ID for the plugin ID.
-   * @param \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface $cck_manager
-   *   The CCK plugin manager.
-   * @param \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface $field_manager
-   *   The field plugin manager.
    * @param bool $translations
    *   Whether or not to include translations.
+   * @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
+   *   The migration field discovery service.
    */
-  public function __construct($base_plugin_id, MigrateCckFieldPluginManagerInterface $cck_manager, MigrateFieldPluginManagerInterface $field_manager, $translations) {
+  public function __construct($base_plugin_id, $translations, FieldDiscoveryInterface $field_discovery) {
     $this->basePluginId = $base_plugin_id;
-    $this->cckPluginManager = $cck_manager;
-    $this->fieldPluginManager = $field_manager;
     $this->includeTranslations = $translations;
+    $this->fieldDiscovery = $field_discovery;
   }
 
   /**
@@ -86,9 +60,8 @@ public static function create(ContainerInterface $container, $base_plugin_id) {
     // Translations don't make sense unless we have content_translation.
     return new static(
       $base_plugin_id,
-      $container->get('plugin.manager.migrate.cckfield'),
-      $container->get('plugin.manager.migrate.field'),
-      $container->get('module_handler')->moduleExists('content_translation')
+      $container->get('module_handler')->moduleExists('content_translation'),
+      $container->get('migrate_drupal.field_discovery')
     );
   }
 
@@ -111,24 +84,6 @@ public function getDerivativeDefinitions($base_plugin_definition) {
       return $this->derivatives;
     }
 
-    $fields = [];
-    try {
-      $source_plugin = static::getSourcePlugin('d7_field_instance');
-      $source_plugin->checkRequirements();
-
-      // Read all field instance definitions in the source database.
-      foreach ($source_plugin as $row) {
-        if ($row->getSourceProperty('entity_type') == 'node') {
-          $fields[$row->getSourceProperty('bundle')][$row->getSourceProperty('field_name')] = $row->getSource();
-        }
-      }
-    }
-    catch (RequirementsException $e) {
-      // If checkRequirements() failed then the field module did not exist and
-      // we do not have any fields. Therefore, $fields will be empty and below
-      // we'll create a migration just for the node properties.
-    }
-
     try {
       foreach ($node_types as $row) {
         $node_type = $row->getSourceProperty('type');
@@ -158,33 +113,9 @@ public function getDerivativeDefinitions($base_plugin_definition) {
           $values['migration_dependencies']['required'][] = 'd7_node:' . $node_type;
         }
 
+        /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
         $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($values);
-        if (isset($fields[$node_type])) {
-          foreach ($fields[$node_type] as $field_name => $info) {
-            $field_type = $info['type'];
-            try {
-              $plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration);
-              if (!isset($this->fieldPluginCache[$field_type])) {
-                $this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($plugin_id, ['core' => 7], $migration);
-              }
-              $this->fieldPluginCache[$field_type]
-                ->defineValueProcessPipeline($migration, $field_name, $info);
-            }
-            catch (PluginNotFoundException $ex) {
-              try {
-                $plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration);
-                if (!isset($this->cckPluginCache[$field_type])) {
-                  $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($plugin_id, ['core' => 7], $migration);
-                }
-                $this->cckPluginCache[$field_type]
-                  ->processCckFieldValues($migration, $field_name, $info);
-              }
-              catch (PluginNotFoundException $ex) {
-                $migration->setProcessOfProperty($field_name, $field_name);
-              }
-            }
-          }
-        }
+        $this->fieldDiscovery->addBundleFieldProcesses($migration, 'node', $node_type);
         $this->derivatives[$node_type] = $migration->getPluginDefinition();
       }
     }
diff --git a/core/modules/taxonomy/src/Plugin/migrate/D7TaxonomyTermDeriver.php b/core/modules/taxonomy/src/Plugin/migrate/D7TaxonomyTermDeriver.php
index 566cf3b3da22..c116f813ede1 100644
--- a/core/modules/taxonomy/src/Plugin/migrate/D7TaxonomyTermDeriver.php
+++ b/core/modules/taxonomy/src/Plugin/migrate/D7TaxonomyTermDeriver.php
@@ -3,20 +3,17 @@
 namespace Drupal\taxonomy\Plugin\migrate;
 
 use Drupal\Component\Plugin\Derivative\DeriverBase;
-use Drupal\Component\Plugin\Exception\PluginNotFoundException;
 use Drupal\Core\Database\DatabaseExceptionWrapper;
 use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
 use Drupal\migrate\Exception\RequirementsException;
 use Drupal\migrate\Plugin\MigrationDeriverTrait;
-use Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface;
-use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
+use Drupal\migrate_drupal\FieldDiscoveryInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Deriver for Drupal 7 taxonomy term migrations based on vocabularies.
  */
 class D7TaxonomyTermDeriver extends DeriverBase implements ContainerDeriverInterface {
-
   use MigrationDeriverTrait;
 
   /**
@@ -27,47 +24,23 @@ class D7TaxonomyTermDeriver extends DeriverBase implements ContainerDeriverInter
   protected $basePluginId;
 
   /**
-   * Already-instantiated cckfield plugins, keyed by ID.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldInterface[]
-   */
-  protected $cckPluginCache;
-
-  /**
-   * The CCK plugin manager.
+   * The migration field discovery service.
    *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface
+   * @var \Drupal\migrate_drupal\FieldDiscoveryInterface
    */
-  protected $cckPluginManager;
-
-  /**
-   * Already-instantiated field plugins, keyed by ID.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateFieldInterface[]
-   */
-  protected $fieldPluginCache;
-
-  /**
-   * The field plugin manager.
-   *
-   * @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
-   */
-  protected $fieldPluginManager;
+  protected $fieldDiscovery;
 
   /**
    * D7TaxonomyTermDeriver constructor.
    *
    * @param string $base_plugin_id
    *   The base plugin ID for the plugin ID.
-   * @param \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface $cck_manager
-   *   The CCK plugin manager.
-   * @param \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface $field_manager
-   *   The field plugin manager.
+   * @param \Drupal\migrate_drupal\FieldDiscoveryInterface $field_discovery
+   *   The migration field discovery service.
    */
-  public function __construct($base_plugin_id, MigrateCckFieldPluginManagerInterface $cck_manager, MigrateFieldPluginManagerInterface $field_manager) {
+  public function __construct($base_plugin_id, FieldDiscoveryInterface $field_discovery) {
     $this->basePluginId = $base_plugin_id;
-    $this->cckPluginManager = $cck_manager;
-    $this->fieldPluginManager = $field_manager;
+    $this->fieldDiscovery = $field_discovery;
   }
 
   /**
@@ -76,8 +49,7 @@ public function __construct($base_plugin_id, MigrateCckFieldPluginManagerInterfa
   public static function create(ContainerInterface $container, $base_plugin_id) {
     return new static(
       $base_plugin_id,
-      $container->get('plugin.manager.migrate.cckfield'),
-      $container->get('plugin.manager.migrate.field')
+      $container->get('migrate_drupal.field_discovery')
     );
   }
 
@@ -85,23 +57,6 @@ public static function create(ContainerInterface $container, $base_plugin_id) {
    * {@inheritdoc}
    */
   public function getDerivativeDefinitions($base_plugin_definition) {
-    $fields = [];
-    try {
-      $source_plugin = static::getSourcePlugin('d7_field_instance');
-      $source_plugin->checkRequirements();
-
-      // Read all field instance definitions in the source database.
-      foreach ($source_plugin as $row) {
-        if ($row->getSourceProperty('entity_type') == 'taxonomy_term') {
-          $fields[$row->getSourceProperty('bundle')][$row->getSourceProperty('field_name')] = $row->getSource();
-        }
-      }
-    }
-    catch (RequirementsException $e) {
-      // If checkRequirements() failed then the field module did not exist and
-      // we do not have any fields. Therefore, $fields will be empty and below
-      // we'll create a migration just for the node properties.
-    }
 
     $vocabulary_source_plugin = static::getSourcePlugin('d7_taxonomy_vocabulary');
     try {
@@ -126,34 +81,9 @@ public function getDerivativeDefinitions($base_plugin_definition) {
         $values['source']['bundle'] = $bundle;
         $values['destination']['default_bundle'] = $bundle;
 
-        /** @var Migration $migration */
+        /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
         $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($values);
-        if (isset($fields[$bundle])) {
-          foreach ($fields[$bundle] as $field_name => $info) {
-            $field_type = $info['type'];
-            try {
-              $plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration);
-              if (!isset($this->fieldPluginCache[$field_type])) {
-                $this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($plugin_id, ['core' => 7], $migration);
-              }
-              $this->fieldPluginCache[$field_type]
-                ->defineValueProcessPipeline($migration, $field_name, $info);
-            }
-            catch (PluginNotFoundException $ex) {
-              try {
-                $plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration);
-                if (!isset($this->cckPluginCache[$field_type])) {
-                  $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($plugin_id, ['core' => 7], $migration);
-                }
-                $this->cckPluginCache[$field_type]
-                  ->processCckFieldValues($migration, $field_name, $info);
-              }
-              catch (PluginNotFoundException $ex) {
-                $migration->setProcessOfProperty($field_name, $field_name);
-              }
-            }
-          }
-        }
+        $this->fieldDiscovery->addBundleFieldProcesses($migration, 'taxonomy_term', $bundle);
         $this->derivatives[$bundle] = $migration->getPluginDefinition();
       }
     }
diff --git a/core/modules/user/src/Plugin/migrate/User.php b/core/modules/user/src/Plugin/migrate/User.php
index 4cf8f1491924..356c4e3b4431 100644
--- a/core/modules/user/src/Plugin/migrate/User.php
+++ b/core/modules/user/src/Plugin/migrate/User.php
@@ -16,47 +16,21 @@ class User extends FieldMigration {
   public function getProcess() {
     if (!$this->init) {
       $this->init = TRUE;
-      $definition['source'] = [
-        'entity_type' => 'user',
-        'ignore_map' => TRUE,
-      ] + $this->source;
-      $definition['destination']['plugin'] = 'null';
-      $definition['idMap']['plugin'] = 'null';
-      if (\Drupal::moduleHandler()->moduleExists('field')) {
-        $definition['source']['plugin'] = 'd7_field_instance';
-        $field_migration = $this->migrationPluginManager->createStubMigration($definition);
-        foreach ($field_migration->getSourcePlugin() as $row) {
-          $field_name = $row->getSourceProperty('field_name');
-          $field_type = $row->getSourceProperty('type');
-          if (empty($field_type)) {
-            continue;
-          }
-          if ($this->fieldPluginManager->hasDefinition($field_type)) {
-            if (!isset($this->fieldPluginCache[$field_type])) {
-              $plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, [], $this);
-              $this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($plugin_id, [], $this);
-            }
-            $info = $row->getSource();
-            $this->fieldPluginCache[$field_type]
-              ->defineValueProcessPipeline($this, $field_name, $info);
-          }
-          else {
-            if ($this->cckPluginManager->hasDefinition($field_type)) {
-              if (!isset($this->cckPluginCache[$field_type])) {
-                $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($field_type, [], $this);
-              }
-              $info = $row->getSource();
-              $this->cckPluginCache[$field_type]
-                ->processCckFieldValues($this, $field_name, $info);
-            }
-            else {
-              $this->process[$field_name] = $field_name;
-            }
-          }
-        }
-      }
+      $this->fieldDiscovery->addEntityFieldProcesses($this, 'user');
+
+      $definition = [
+        'source' => [
+          'plugin' => 'profile_field',
+          'ignore_map' => TRUE,
+        ],
+        'idMap' => [
+          'plugin' => 'null',
+        ],
+        'destination' => [
+          'plugin' => 'null',
+        ],
+      ];
       try {
-        $definition['source']['plugin'] = 'profile_field';
         $profile_migration = $this->migrationPluginManager->createStubMigration($definition);
         // Ensure that Profile is enabled in the source DB.
         $profile_migration->checkRequirements();
-- 
GitLab