Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #3277: Site-specific drush.yml file not loaded. #3278

Closed
wants to merge 10 commits into from
2 changes: 1 addition & 1 deletion isolation/tests/ConfigLocatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ protected function createConfigLocator($isLocal = false, $configPath = '')
// Make our environment settings available as configuration items
$configLocator->addEnvironment($this->environment());

$configLocator->addSitewideConfig($this->siteDir());
$configLocator->addDrupalConfig($this->siteDir());

return $configLocator;
}
Expand Down
22 changes: 1 addition & 21 deletions src/Commands/core/SiteInstallCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ public function pre(CommandData $commandData)
// a fallback name when '--sites-subdir' is not specified, but
// only if the uri and the folder name match, and only if
// the sites directory has already been created.
$dir = $this->getSitesSubdirFromUri($root, $aliasRecord->get('uri'));
$dir = StringUtils::lookupSiteDirFromUri($aliasRecord->get('uri'), $root);
Copy link
Contributor Author

@grasmash grasmash Dec 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we fall back to 'default' if the URI does not have a matching entry in sites.php?

If I have a single site in sites/default and I have drush.yml file that sets options.uri: http://example.com, I expect drush si to install the site even if I don't have a sites.php file.

}

if (!$dir) {
Expand Down Expand Up @@ -338,26 +338,6 @@ public function pre(CommandData $commandData)
}
}

/**
* Determine an appropriate site subdir name to use for the
* provided uri.
*/
protected function getSitesSubdirFromUri($root, $uri)
{
$dir = strtolower($uri);
// Always accept simple uris (e.g. 'dev', 'stage', etc.)
if (preg_match('#^[a-z0-9_-]*$#', $dir)) {
return $dir;
}
// Strip off the protocol from the provided uri -- however,
// now we will require that the sites subdir already exist.
$dir = preg_replace('#[^/]*/*#', '', $dir);
if (file_exists(Path::join($root, $dir))) {
return $dir;
}
return false;
}

/**
* Fake the necessary HTTP headers that the Drupal installer still needs:
* @see https://github.com/drupal/drupal/blob/d260101f1ea8a6970df88d2f1899248985c499fc/core/includes/install.core.inc#L287
Expand Down
61 changes: 51 additions & 10 deletions src/Config/ConfigLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Drush\Config\Loader\YamlConfigLoader;
use Consolidation\Config\Loader\ConfigProcessor;
use Consolidation\Config\Util\EnvConfig;
use Drush\Utils\StringUtils;
use Symfony\Component\Finder\Finder;

/**
Expand Down Expand Up @@ -37,7 +38,9 @@ class ConfigLocator

protected $sources = false;

protected $siteRoots = [];
protected $drupalRoots = [];

protected $siteDirs = [];

protected $composerRoot;

Expand Down Expand Up @@ -101,7 +104,7 @@ public function __construct($envPrefix = '')
}
$this->config->addPlaceholder(self::USER_CONTEXT);
$this->config->addPlaceholder(self::DRUPAL_CONTEXT);
$this->config->addPlaceholder(self::SITE_CONTEXT); // not implemented yet (multisite)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n.b. "not implemented yet (mulitsite)" comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was removed intentionally, as this PR adds site support.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I made that note for @weitzman to point out that this PR is implementing a TODO. Then I forgot to save the comment that referenced it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it did get saved; see above.

$this->config->addPlaceholder(self::SITE_CONTEXT);
$this->config->addPlaceholder(self::ALIAS_CONTEXT);
$this->config->addPlaceholder(self::PREFLIGHT_CONTEXT);
$this->config->addPlaceholder(self::ENVIRONMENT_CONTEXT);
Expand Down Expand Up @@ -256,26 +259,64 @@ public function addDrushConfig($drushProjectDir)
* Add any configuration files found around the Drupal root of the
* selected site.
*
* @param Path to the selected Drupal site
* @param $drupalRoot
* Path to the selected Drupal site.
* @return $this
*/
public function addSitewideConfig($siteRoot)
public function addDrupalConfig($drupalRoot)
{
// There might not be a site.
if (!is_dir($siteRoot)) {
if (!is_dir($drupalRoot)) {
return;
}

// We might have already processed this root.
$siteRoot = realpath($siteRoot);
if (in_array($siteRoot, $this->siteRoots)) {
$drupalRoot = realpath($drupalRoot);
if (in_array($drupalRoot, $this->drupalRoots)) {
return;
}

// Remember that we've seen this location.
$this->siteRoots[] = $siteRoot;
$this->drupalRoots[] = $drupalRoot;

$this->addConfigPaths(self::DRUPAL_CONTEXT, [ dirname($drupalRoot) . '/drush', "$drupalRoot/drush", "$drupalRoot/sites/all/drush" ]);
return $this;
}

/**
* Add any configuration files found around the multisite directory.
*
* @param \Drush\SiteAlias\AliasRecord $alias
* Site URI of the multisite.
*
* @return $this
*/
public function addSiteConfig($alias)
{
$uri = $alias->uri() ?: 'default';

// Parse a fqdn and look for matching entry in sites/sites.php.
if (filter_var($uri, FILTER_VALIDATE_URL)) {
if ($dir_name = StringUtils::lookupSiteDirFromUri($uri, $alias->root())) {
$uri = $dir_name;
}
}

// There might not be a site directory.
$site_dir = $alias->root() ."/sites/$uri";
if (!is_dir($site_dir)) {
return;
}

// We might have already processed this site.
if (in_array($site_dir, $this->siteDirs)) {
return;
}

// Remember that we've seen this site.
$this->siteDirs[] = $site_dir;

$this->addConfigPaths(self::DRUPAL_CONTEXT, [ dirname($siteRoot) . '/drush', "$siteRoot/drush", "$siteRoot/sites/all/drush" ]);
$this->addConfigPaths(self::SITE_CONTEXT, [ "$site_dir", "$site_dir/drush" ]);
return $this;
}

Expand Down Expand Up @@ -381,7 +422,7 @@ public function getSiteAliasPaths($paths, Environment $environment)
{
// In addition to the paths passed in to us (from --alias-paths
// commandline options), add some site-local locations.
$base_dirs = array_filter(array_merge($this->siteRoots, [$this->composerRoot]));
$base_dirs = array_filter(array_merge($this->drupalRoots, [$this->composerRoot]));
$site_local_paths = array_map(
function ($item) {
return "$item/drush/sites";
Expand Down
6 changes: 4 additions & 2 deletions src/Preflight/Preflight.php
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ public function preflight($argv)
// Extend configuration and alias files to include files in
// target site.
$root = $this->findSelectedSite();
$this->configLocator->addSitewideConfig($root);
$this->configLocator->addDrupalConfig($root);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Drush8, we don't load any site specific config until the Site phase of the bootstrap. I'd like to keep consistent with that in Drush9. Since this change is in Preflight (i.e. happens before bootstrap), I dont think its properly placed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that moving all config processing to preflight is an improvement. When config was php, delaying loading site-specific config until site bootstrap has the benefit of allowing folks to use Drupal APIs in their config code. With yaml config, I'm not sure that there is a benefit to postponing config loading. Loading config in preflight might allow folks to use --uri=foo and set URI to https://foo.example.com in their drush.yml file, to give one example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is exactly my use case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are working around the problem that config is static after its loaded. It would be preferable IMO if uri were discovered during SITE phase and then used for link building and such during command execution.

We already do ROOT discovery during Preflight. This PR effectively moves SITE discovery there too. It begs the question of how much do we need the progressive Bootstrap that we do. Maybe NONE and FULL is all we need? We don't need to figure this out now but it would be good to get some clarity on future directions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that site selection is already happening in preflight. Note the comment removed in this PR: "// not implemented yet (multisite)" (see my comment #3278 (review)). I think this PR is essentially implementing a TODO. I still don't see any practical benefit to postponing uri discovery to the SITE phase, and as previously mentioned, there are some disadvantages to doing that.

Regarding progressive bootstrap, for command discovery we only need NONE (built-in and global commands) and FULL (module commands). Built-in and global commands can also declare the bootstrap level they want, but it is not possible for them to declare their bootstrap level, because we already have to bootstrap to FULL in order to find them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the current design, the config locator is not available during bootstrap.

Does it not currently work to set $options['uri'] from a site-specific drushrc.php file in Drush 8 with Drupal 8?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont know. If so, we just accept that as a regression with known workarounds (use an alias, use site-set, specify uri on the request, ...).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree. While I'd like to see this feature work, I think it's enough of an edge case that I'd like the solution to be clean (keep Drupal-specific code in the bootstrap phase).

Copy link
Member

@weitzman weitzman Feb 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have found one more workaround. If you set the environment variable DRUSH_OPTIONS_URI=http://example.com then your custom uri will be used. No need to fuss with Drush config or aliases. For Docker/Vagrant sites, this is an easy solution.

So, how to set that env variable? If you have many sites on same host, or don't control the server, this can be challenging.

  1. Anyone used direnv?
  2. I'm using PHPEnv successfully on Drupal projects but at this time there is no way to load custom code early enough in a Drush request for 'uri' to take effect. That happens at end of Runtime https://github.com/drush-ops/drush/blob/master/src/Runtime/Runtime.php#L88-L93

I will now writeup docs for #3009 as I now understand that system.

Copy link
Member

@weitzman weitzman Mar 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a follow-up, the drupal-project will soon support .env files and documents how to set DRUSH_OPTIONS_URI so that site specific works. See drupal-composer/drupal-project#351. You can implement that in your composer project even before this PR is merged.

$this->configLocator->setComposerRoot($this->drupalFinder()->getComposerRoot());

// Look up the locations where alias files may be found.
Expand All @@ -276,7 +276,9 @@ public function preflight($argv)

// If we did not redispatch, then add the site-wide config for the
// new root (if the root did in fact change) and continue.
$this->configLocator->addSitewideConfig($root);
$this->configLocator->addDrupalConfig($root);
// Set multisite config using uri from alias.
$this->configLocator->addSiteConfig($selfAliasRecord);

// Remember the paths to all the files we loaded, so that we can
// report on it from Drush status or wherever else it may be needed.
Expand Down
38 changes: 38 additions & 0 deletions src/Utils/StringUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,42 @@ public static function generatePassword($length = 10)

return $pass;
}

/**
* Lookup a site's directory via the sites.php file given a uri.
*
* @param string $uri
* The site URI.
* @return string $drupalRoot
* The directory associated with that URI.
* @param bool $require_settings
* Only directories with an existing settings.php file will be recognized.
* Defaults to TRUE.
* @see \Drupal\Core\DrupalKernel::findSitePath()
*/
public static function lookupSiteDirFromUri($uri, $drupalRoot, $require_settings = true)
{
if (file_exists($drupalRoot . '/sites/sites.php')) {
$sites = [];
// This will overwrite $sites with the desired mappings.
include($drupalRoot . '/sites/sites.php');

// This code is adapted from
// \Drupal\Core\DrupalKernel::findSitePath().
$path = explode('/', parse_url($uri, PHP_URL_PATH));
$server = explode('.', implode('.', array_reverse(explode(':', rtrim(parse_url($uri, PHP_URL_HOST), '.')))));
for ($i = count($path); $i > 0; $i--) {
for ($j = count($server); $j > 0; $j--) {
$dir = implode('.', array_slice($server, -$j)) . implode('.', array_slice($path, 0, $i));
if (isset($sites[$dir]) && file_exists($drupalRoot . '/sites/' . $sites[$dir])) {
$dir = $sites[$dir];
}
if (file_exists($drupalRoot . '/sites/' . $dir . '/settings.php') || (!$require_settings && file_exists($drupalRoot . '/sites/' . $dir))) {
return "$dir";
}
}
}
}
return false;
}
}