Skip to content

Commit

Permalink
Merge pull request #25 from tutu-ru/etcd-node-exporter
Browse files Browse the repository at this point in the history
Etcd node exporter
  • Loading branch information
joostfaassen authored Mar 27, 2019
2 parents 9f35a13 + e280e9c commit bb17b4f
Show file tree
Hide file tree
Showing 10 changed files with 502 additions and 25 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ Recursively delete a key and all child keys:
$ bin/etcd-php etcd:rmdir /path/to/dir --recursive
```
#### Export node
```bash
$ bin/etcd-php etcd:export --server=http://127.0.0.1:2379 --format=json --output=config.json /path/to/dir
```
#### Watching for changes
Watch for only the next change on a key:
Expand Down
1 change: 1 addition & 0 deletions bin/etcd-php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ $application->add(new \LinkORB\Component\Etcd\Command\EtcdRmdirCommand());
$application->add(new \LinkORB\Component\Etcd\Command\EtcdLsCommand());
$application->add(new \LinkORB\Component\Etcd\Command\EtcdUpdateDirCommand());
$application->add(new \LinkORB\Component\Etcd\Command\EtcdWatchCommand());
$application->add(new \LinkORB\Component\Etcd\Command\EtcdExportCommand());
$application->run();
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
],
"require": {
"php": ">=5.6",
"symfony/console": "^2.4 || ^3.0",
"symfony/console": "^2.4 || ^3.0 || ^4.0",
"symfony/filesystem": "^2.4 || ^3.0 || ^4.0",
"guzzlehttp/guzzle": "^6.3"
},
"require-dev": {
Expand Down
109 changes: 109 additions & 0 deletions src/Command/EtcdExportCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace LinkORB\Component\Etcd\Command;

use LinkORB\Component\Etcd\Client as EtcdClient;
use LinkORB\Component\Etcd\DirectoryExporter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Yaml\Yaml;

class EtcdExportCommand extends Command
{
const PATH_SEPARATOR = '/';

const FORMAT_JSON = 'json';
const FORMAT_YAML = 'yaml';
const FORMAT_DOTENV = 'dotenv';

protected function configure()
{
$this
->setName('etcd:export')
->setDescription(
'Export a directory'
)
->addArgument(
'key',
InputArgument::REQUIRED,
'Dir to export'
)->addOption(
'output',
'o',
InputOption::VALUE_REQUIRED,
'json file to save dump'
)->addOption(
'format',
'f',
InputOption::VALUE_REQUIRED,
'json file to save dump',
self::FORMAT_JSON
)->addOption(
'server',
's',
InputOption::VALUE_REQUIRED,
'Base url of etcd server',
'http://127.0.0.1:2379'
);
}

public function execute(InputInterface $input, OutputInterface $output)
{
$key = $input->getArgument('key');
$server = $input->getOption('server');

$client = new EtcdClient($server);
$dirExporter = new DirectoryExporter($client);

switch ($input->getOption('format')) {
case self::FORMAT_JSON:
$result = $this->createJson($dirExporter, $key);
break;
case self::FORMAT_YAML:
$result = $this->createYaml($dirExporter, $key);
break;
case self::FORMAT_DOTENV:
$result = $this->createDotEnv($dirExporter, $key);
break;
default:
throw new \RuntimeException('Unknown format: ' . $input->getOption('format'));
}

$file = $input->getOption('output');
if (!is_null($file)) {
$fs = new Filesystem();
$fs->dumpFile($file, $result . PHP_EOL);
} else {
$output->writeln($result);
}
}


private function createJson(DirectoryExporter $directoryExporter, $key)
{
$data = $directoryExporter->exportArray($key);
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}


private function createYaml(DirectoryExporter $directoryExporter, $key)
{
$data = $directoryExporter->exportArray($key);
return Yaml::dump($data);
}


private function createDotEnv(DirectoryExporter $directoryExporter, $key)
{
$data = $directoryExporter->exportKeyValuePairs($key, true);
$result = [];
foreach ($data as $k => $v) {
$result[] = strtoupper(str_replace("/", "_", $k)) . '=' . $v;
}
return implode(PHP_EOL, $result);
}
}
102 changes: 102 additions & 0 deletions src/DirectoryExporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace LinkORB\Component\Etcd;

class DirectoryExporter
{
const PATH_SEPARATOR = '/';

/** @var Client */
private $client;

public function __construct(Client $client)
{
$this->client = $client;
}


public function exportArray($directory)
{
$lsResult = $this->client->listDir($directory, true);
$rootPath = $lsResult['node']['key'];

$kvList = $this->createKeyValuePairs($lsResult);

$result = [];
if (count($kvList) === 1 && array_keys($kvList)[0] === $rootPath) {
// $dir is property
$parts = explode(self::PATH_SEPARATOR, $rootPath);
$result[$parts[count($parts) - 1]] = $kvList[$rootPath];
} else {
foreach ($kvList as $k => $v) {
$realKey = substr($k, strlen($rootPath));
$parts = explode(self::PATH_SEPARATOR, $realKey);
array_shift($parts); // remove first empty element
$this->addToDepth($result, $parts, $v);
}
}
return $result;
}


public function exportKeyValuePairs($dir, $recursive = true)
{
$lsResult = $this->client->listDir($dir, $recursive);
$rootPath = $lsResult['node']['key'];

$kvList = $this->createKeyValuePairs($lsResult);

$result = [];
if (count($kvList) === 1 && array_keys($kvList)[0] === $rootPath) {
// $dir is property
$parts = explode(self::PATH_SEPARATOR, $rootPath);
$result[$parts[count($parts) - 1]] = $kvList[$rootPath];
} else {
foreach ($kvList as $key => $value) {
$result[substr($key, strlen($rootPath) + 1)] = $value;
}
}
return $result;
}


private function createKeyValuePairs($lsResult)
{
$result = [];
$iterator = new \RecursiveArrayIterator($lsResult);
$this->traverse($result, $iterator);
ksort($result);
return $result;
}


private function traverse(&$values, \RecursiveArrayIterator $iterator)
{
while ($iterator->valid()) {
if ($iterator->hasChildren()) {
$this->traverse($values, $iterator->getChildren());
} else {
$currentLevel = $iterator->getArrayCopy();
if (array_key_exists('key', $currentLevel) && array_key_exists('value', $currentLevel)) {
$values[$currentLevel['key']] = $currentLevel['value'];
return;
}
}
$iterator->next();
}
}


private function addToDepth(&$array, $path, $value)
{
if (1 === count($path)) {
$array[current($path)] = $value;
} else {
$current = array_shift($path);
if (!array_key_exists($current, $array)) {
$array[$current] = [];
}
$this->addToDepth($array[$current], $path, $value);
}
}
}
28 changes: 28 additions & 0 deletions tests/BaseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace LinkORB\Tests\Component\Etcd;

use LinkORB\Component\Etcd\Client;
use PHPUnit\Framework\TestCase;

abstract class BaseTest extends TestCase
{
/** @var Client */
protected $client;

protected $dirname = '/phpunit_test';


protected function setUp()
{
$this->client = new Client();
$this->client->mkdir($this->dirname);
$this->client->setRoot($this->dirname);
}

protected function tearDown()
{
$this->client->setRoot('/');
$this->client->rmdir($this->dirname, true);
}
}
25 changes: 1 addition & 24 deletions tests/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,8 @@

namespace LinkORB\Tests\Component\Etcd;

use LinkORB\Component\Etcd\Client;
use PHPUnit\Framework\TestCase;

class ClientTest extends TestCase
class ClientTest extends BaseTest
{
/**
* @var Client
*/
protected $client;

private $dirname = '/phpunit_test';

protected function setUp()
{
$this->client = new Client();
$this->client->mkdir($this->dirname);
$this->client->setRoot($this->dirname);
}

protected function tearDown()
{
$this->client->setRoot('/');
$this->client->rmdir($this->dirname, true);
}

/**
* @covers LinkORB\Component\Etcd\Client::getVersion
*/
Expand Down
69 changes: 69 additions & 0 deletions tests/DirectoryExporterArrayTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace LinkORB\Tests\Component\Etcd;

use GuzzleHttp\Exception\ClientException;
use LinkORB\Component\Etcd\DirectoryExporter;

class DirectoryExporterArrayTest extends BaseTest
{
use FixtureTrait;

protected function setUp()
{
parent::setUp();
$this->prepareFixture($this->client);
}

private function getDirectoryExporter()
{
return new DirectoryExporter($this->client);
}


public function testFailOnNotExistingKey()
{
$this->expectException(ClientException::class);
$this->expectExceptionCode(404);
$this->getDirectoryExporter()->exportArray(__FUNCTION__);
}


public function testRootDir()
{
$res = $this->getDirectoryExporter()->exportArray('/');
$this->assertEquals($this->getExpectedFullTreeAsArray(), $res);
}


public function testDir()
{
$res = $this->getDirectoryExporter()->exportArray('/dir');
$this->assertEquals($this->getExpectedDirAsArray(), $res);
}


public function testSubDir()
{
$res = $this->getDirectoryExporter()->exportArray('/dir/sub2/');
$this->assertEquals(
[
'f1' => 'vs2_1',
'f2' => 'vs2_2',
],
$res
);
}


public function testProperty()
{
$result = $this->getDirectoryExporter()->exportArray('/dir/sub2/f1');
$this->assertEquals(
[
'f1' => 'vs2_1',
],
$result
);
}
}
Loading

0 comments on commit bb17b4f

Please sign in to comment.