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

Add debug info for the session storage #1462

Merged
merged 1 commit into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/CredentialHelper/KeyringUnavailableException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Platformsh\Cli\CredentialHelper;

use Platformsh\Cli\Util\OsUtil;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Exception\ProcessTimedOutException;

/**
* An exception thrown when the login keyring is unavailable.
*/
class KeyringUnavailableException extends \RuntimeException
{
public static function fromTimeout(ProcessTimedOutException $_)
{
$type = OsUtil::isOsX() ? 'keychain' : 'keyring';
$message = sprintf('The credential helper process timed out while trying to access the %s.', $type);
$message .= "\n" . sprintf('This may be due to a system password prompt: is the login %s unlocked?', $type);
return new KeyringUnavailableException($message);
}

public static function fromFailure(ProcessFailedException $e)
{
$type = OsUtil::isOsX() ? 'keychain' : 'keyring';
$message = sprintf('The credential helper process failed while trying to access the %s.', $type);
$process = $e->getProcess();
if ($process->getExitCode() === 2 && strpos($process->getErrorOutput(), 'libsecret-CRITICAL') !== false) {
$message .= "\n" . sprintf('This can happen when the password dialog is dismissed. Is the login %s unlocked?', $type);
} else {
$message .= "\n" . $e->getMessage();
}
return new KeyringUnavailableException($message);
}
}
57 changes: 40 additions & 17 deletions src/Service/Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
use GuzzleHttp\Event\ErrorEvent;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Message\ResponseInterface;
use Platformsh\Cli\CredentialHelper\KeyringUnavailableException;
use Platformsh\Cli\CredentialHelper\Manager;
use Platformsh\Cli\CredentialHelper\SessionStorage;
use Platformsh\Cli\CredentialHelper\SessionStorage as CredentialHelperStorage;
use Platformsh\Cli\Event\EnvironmentsChangedEvent;
use Platformsh\Cli\Event\LoginRequiredEvent;
use Platformsh\Cli\Exception\ProcessFailedException;
use Platformsh\Cli\GuzzleDebugSubscriber;
use Platformsh\Cli\Model\Route;
use Platformsh\Cli\Util\NestedArrayUtil;
Expand Down Expand Up @@ -44,6 +46,7 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Process\Exception\ProcessTimedOutException;

/**
* Decorates the PlatformClient API client to provide aggressive caching.
Expand Down Expand Up @@ -168,7 +171,7 @@ public function hasApiToken($includeStored = true)
public function listSessionIds()
{
$ids = [];
if ($this->sessionStorage instanceof SessionStorage) {
if ($this->sessionStorage instanceof CredentialHelperStorage) {
$ids = $this->sessionStorage->listSessionIds();
}
$dir = $this->config->getSessionDir();
Expand All @@ -192,7 +195,7 @@ public function listSessionIds()
*/
public function anySessionsExist()
{
if ($this->sessionStorage instanceof SessionStorage && $this->sessionStorage->hasAnySessions()) {
if ($this->sessionStorage instanceof CredentialHelperStorage && $this->sessionStorage->hasAnySessions()) {
return true;
}
$dir = $this->config->getSessionDir();
Expand Down Expand Up @@ -231,7 +234,7 @@ public function logout()
*/
public function deleteAllSessions()
{
if ($this->sessionStorage instanceof SessionStorage) {
if ($this->sessionStorage instanceof CredentialHelperStorage) {
$this->sessionStorage->deleteAll();
}
$dir = $this->config->getSessionDir();
Expand Down Expand Up @@ -550,8 +553,20 @@ public function getClient($autoLogin = true, $reset = false)
// (unless an access token was set directly).
if (!isset($options['api_token']) || $options['api_token_type'] !== 'access') {
$this->initSessionStorage();
// This will load from the session for the first time.
$session->setStorage($this->sessionStorage);
$this->debug('Loading session');
try {
$session->setStorage($this->sessionStorage);
} catch (\RuntimeException $e) {
if ($this->sessionStorage instanceof CredentialHelperStorage) {
$previous = $e->getPrevious();
if ($previous instanceof ProcessTimedOutException) {
throw KeyringUnavailableException::fromTimeout($previous);
} elseif ($previous instanceof ProcessFailedException) {
throw KeyringUnavailableException::fromFailure($previous);
}
}
throw $e;
}
}

$connector = new Connector($options, $session);
Expand Down Expand Up @@ -584,16 +599,24 @@ public function getClient($autoLogin = true, $reset = false)
* Initializes session credential storage.
*/
private function initSessionStorage() {
// Attempt to use the docker-credential-helpers.
$manager = new Manager($this->config);
if ($manager->isSupported()) {
$manager->install();
$this->sessionStorage = new SessionStorage($manager, $this->config->get('application.slug'));
return;
}
if (!isset($this->sessionStorage)) {
// Attempt to use the docker-credential-helpers.
$manager = new Manager($this->config);
if ($manager->isSupported()) {
if ($manager->isInstalled()) {
$this->debug('Using Docker credential helper for session storage');
} else {
$this->debug('Installing Docker credential helper for session storage');
$manager->install();
}
$this->sessionStorage = new CredentialHelperStorage($manager, $this->config->get('application.slug'));
return;
}

// Fall back to file storage.
$this->sessionStorage = new File($this->config->getSessionDir());
// Fall back to file storage.
$this->debug('Using filesystem for session storage');
$this->sessionStorage = new File($this->config->getSessionDir());
}
}

/**
Expand Down Expand Up @@ -961,9 +984,9 @@ public function getUser($id = null, $reset = false)
}
$this->cache->save($cacheKey, $user->getData(), (int) $this->config->getWithDefault('api.users_ttl', 600));
} else {
$this->debug('Loaded user info from cache: ' . $id);
$connector = $this->getClient()->getConnector();
$user = new User($data, $connector->getApiUrl() . '/users', $connector->getClient());
$this->debug('Loaded user info from cache: ' . $id);
}
return $user;
}
Expand Down Expand Up @@ -1273,8 +1296,8 @@ public function getCurrentDeployment(Environment $environment, $refresh = false,
$data['_uri'] = $deployment->getUri();
$this->cache->save($cacheKey, $data);
} else {
$deployment = new EnvironmentDeployment($data, $data['_uri'], $this->getHttpClient(), true);
$this->debug('Loaded environment deployment from cache for environment: ' . $environment->id);
$deployment = new EnvironmentDeployment($data, $data['_uri'], $this->getHttpClient(), true);
}

return self::$deploymentsCache[$cacheKey] = $deployment;
Expand Down