Scan for config in /etc and ~, improve automation command

This commit is contained in:
Niklas Keller
2016-06-03 19:38:47 +02:00
parent 3472bd1b3c
commit 583318fa0b
4 changed files with 90 additions and 57 deletions

View File

@@ -8,6 +8,7 @@ use Amp\Process;
use Kelunik\Acme\AcmeException;
use League\CLImate\Argument\Manager;
use League\CLImate\CLImate;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
class Auto implements Command {
@@ -38,6 +39,10 @@ class Auto implements Command {
$this->climate->error("Config file ({$configPath}) not found.");
yield new CoroutineResult(1);
return;
} catch (ParseException $e) {
$this->climate->error("Config file ({$configPath}) had an invalid format and couldn't be parsed.");
yield new CoroutineResult(1);
return;
}
if (!isset($config["email"])) {
@@ -46,20 +51,12 @@ class Auto implements Command {
return;
}
if (!isset($config["certificates"])) {
$this->climate->error("Config file ({$configPath}) didn't have a 'certificates' section.");
if (!isset($config["certificates"]) || !is_array($config["certificates"])) {
$this->climate->error("Config file ({$configPath}) didn't have a 'certificates' section that's an array.");
yield new CoroutineResult(2);
return;
}
if (isset($config["storage"])) {
$storage = $config["storage"];
}
if (isset($config["server"])) {
$server = $config["server"];
}
$command = implode(" ", array_map("escapeshellarg", [
PHP_BINARY,
$GLOBALS["argv"][0],
@@ -73,27 +70,36 @@ class Auto implements Command {
]));
$process = new Process($command);
$result = (yield $process->exec());
$result = (yield $process->exec(Process::BUFFER_ALL));
if ($result->exit !== 0) {
$this->climate->error("Registration failed ({$result->exit})");
$this->climate->error($command);
$this->climate->br()->out($result->out);
$this->climate->br()->error($result->err);
yield new CoroutineResult(3);
return;
}
$promises = [];
$certificateChunks = array_chunk($config["certificates"], 10);
foreach ($config["certificates"] as $certificate) {
$promises[] = \Amp\resolve($this->checkAndIssue($certificate, $server, $storage));
$errors = [];
foreach ($certificateChunks as $chunk) {
$promises = [];
foreach ($chunk as $certificate) {
$promises[] = \Amp\resolve($this->checkAndIssue($certificate, $server, $storage));
}
list($errors) = (yield \Amp\any($promises));
$errors = array_merge($errors, $errors);
}
list($errors) = (yield \Amp\any($promises));
if (!empty($errors)) {
foreach ($errors as $i => $error) {
$certificate = $config["certificates"][$i];
$this->climate->error("Issuance for the following domains failed: " . implode(", ", array_keys($this->toDomainPathMap((array) $certificate->paths))));
$this->climate->error("Issuance for the following domains failed: " . implode(", ", array_keys($this->toDomainPathMap($certificate["paths"]))));
$this->climate->error("Reason: {$error}");
}
@@ -110,7 +116,7 @@ class Auto implements Command {
* @throws AcmeException if something does wrong
*/
private function checkAndIssue(array $certificate, $server, $storage) {
$domainPathMap = $this->toDomainPathMap((array) $certificate["paths"]);
$domainPathMap = $this->toDomainPathMap($certificate["paths"]);
$commonName = reset(array_keys($domainPathMap));
$args = [
@@ -137,7 +143,6 @@ class Auto implements Command {
if ($result->exit === 1) {
// Renew certificate
$args = [
PHP_BINARY,
$GLOBALS["argv"][0],
@@ -146,9 +151,9 @@ class Auto implements Command {
$server,
"--storage",
$storage,
"-d",
"--domains",
implode(",", array_keys($domainPathMap)),
"-p",
"--path",
implode(PATH_SEPARATOR, array_values($domainPathMap)),
];
@@ -180,17 +185,15 @@ class Auto implements Command {
private function toDomainPathMap(array $paths) {
$result = [];
foreach ($paths as $pathDomainMap) {
foreach ($pathDomainMap as $path => $domains) {
$domains = (array) $domains;
foreach ($paths as $path => $domains) {
$domains = (array) $domains;
foreach ($domains as $domain) {
if (isset($result[$domain])) {
throw new \LogicException("Duplicate domain: {$domain}");
}
$result[$domain] = $path;
foreach ($domains as $domain) {
if (isset($result[$domain])) {
throw new \LogicException("Duplicate domain: {$domain}");
}
$result[$domain] = $path;
}
}
@@ -198,7 +201,7 @@ class Auto implements Command {
}
public static function getDefinition() {
return [
$args = [
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
"config" => [
@@ -208,5 +211,14 @@ class Auto implements Command {
"required" => true,
],
];
$configPath = \Kelunik\AcmeClient\getConfigPath();
if ($configPath) {
$args["config"]["required"] = false;
$args["config"]["defaultValue"] = $configPath;
}
return $args;
}
}

View File

@@ -85,8 +85,6 @@ class Setup implements Command {
}
public static function getDefinition() {
return [
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),

5
src/ConfigException.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
namespace Kelunik\AcmeClient;
class ConfigException extends \Exception { }

View File

@@ -111,43 +111,59 @@ function normalizePath($path) {
return rtrim(str_replace("\\", "/", $path), "/");
}
/**
* Gets the most appropriate config path to use.
*
* @return string|null Resolves to the config path or null.
*/
function getConfigPath() {
$paths = isPhar() ? [substr(dirname(Phar::running(true)), strlen("phar://")) . "acme-client.yml"] : [];
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
if ($home = getenv("HOME")) {
$paths[] = $home . "/.acme-client.yml";
}
$paths[] = "/etc/acme-client.yml";
}
do {
$path = array_shift($paths);
if (file_exists($path)) {
return $path;
}
} while (count($paths));
return null;
}
/**
* Returns a consistent argument description for CLIMate. Valid arguments are "server" and "storage".
*
* @param string $argument argument name
* @return array CLIMate argument description
* @throws AcmeException if the provided acme-client.yml file is invalid
* @throws ConfigException if the provided configuration file is invalid
*/
function getArgumentDescription($argument) {
$isPhar = \Kelunik\AcmeClient\isPhar();
$config = [];
if ($isPhar) {
$configPath = substr(dirname(Phar::running(true)), strlen("phar://")) . "/acme-client.yml";
if ($configPath = getConfigPath()) {
$configContent = file_get_contents($configPath);
if (file_exists($configPath)) {
$configContent = file_get_contents($configPath);
try {
$config = Yaml::parse($configContent);
try {
$value = Yaml::parse($configContent);
if (isset($value["server"]) && is_string($value["server"])) {
$config["server"] = $value["server"];
unset($value["server"]);
}
if (isset($value["storage"]) && is_string($value["storage"])) {
$config["storage"] = $value["storage"];
unset($value["storage"]);
}
if (!empty($value)) {
throw new AcmeException("Provided YAML file had unknown options: " . implode(", ", array_keys($value)));
}
} catch (ParseException $e) {
throw new AcmeException("Unable to parse the YAML file ({$configPath}): " . $e->getMessage());
if (isset($config["server"]) && !is_string($config["server"])) {
throw new ConfigException("'server' set, but not a string.");
}
if (isset($config["storage"]) && !is_string($config["storage"])) {
throw new ConfigException("'storage' set, but not a string.");
}
} catch (ParseException $e) {
throw new AcmeException("Unable to parse the configuration ({$configPath}): " . $e->getMessage());
}
}
@@ -168,6 +184,8 @@ function getArgumentDescription($argument) {
return $argument;
case "storage":
$isPhar = isPhar();
$argument = [
"longPrefix" => "storage",
"description" => "Storage directory for account keys and certificates.",