Skip to content

Commit bb17b4f

Browse files
authored
Merge pull request #25 from tutu-ru/etcd-node-exporter
Etcd node exporter
2 parents 9f35a13 + e280e9c commit bb17b4f

10 files changed

Lines changed: 502 additions & 25 deletions

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ Recursively delete a key and all child keys:
155155
$ bin/etcd-php etcd:rmdir /path/to/dir --recursive
156156
```
157157
158+
#### Export node
159+
160+
```bash
161+
$ bin/etcd-php etcd:export --server=http://127.0.0.1:2379 --format=json --output=config.json /path/to/dir
162+
```
163+
158164
#### Watching for changes
159165
160166
Watch for only the next change on a key:

bin/etcd-php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ $application->add(new \LinkORB\Component\Etcd\Command\EtcdRmdirCommand());
3131
$application->add(new \LinkORB\Component\Etcd\Command\EtcdLsCommand());
3232
$application->add(new \LinkORB\Component\Etcd\Command\EtcdUpdateDirCommand());
3333
$application->add(new \LinkORB\Component\Etcd\Command\EtcdWatchCommand());
34+
$application->add(new \LinkORB\Component\Etcd\Command\EtcdExportCommand());
3435
$application->run();

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
],
2020
"require": {
2121
"php": ">=5.6",
22-
"symfony/console": "^2.4 || ^3.0",
22+
"symfony/console": "^2.4 || ^3.0 || ^4.0",
23+
"symfony/filesystem": "^2.4 || ^3.0 || ^4.0",
2324
"guzzlehttp/guzzle": "^6.3"
2425
},
2526
"require-dev": {

src/Command/EtcdExportCommand.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
namespace LinkORB\Component\Etcd\Command;
4+
5+
use LinkORB\Component\Etcd\Client as EtcdClient;
6+
use LinkORB\Component\Etcd\DirectoryExporter;
7+
use Symfony\Component\Console\Command\Command;
8+
use Symfony\Component\Console\Input\InputArgument;
9+
use Symfony\Component\Console\Input\InputInterface;
10+
use Symfony\Component\Console\Input\InputOption;
11+
use Symfony\Component\Console\Output\OutputInterface;
12+
use Symfony\Component\Filesystem\Filesystem;
13+
use Symfony\Component\Yaml\Yaml;
14+
15+
class EtcdExportCommand extends Command
16+
{
17+
const PATH_SEPARATOR = '/';
18+
19+
const FORMAT_JSON = 'json';
20+
const FORMAT_YAML = 'yaml';
21+
const FORMAT_DOTENV = 'dotenv';
22+
23+
protected function configure()
24+
{
25+
$this
26+
->setName('etcd:export')
27+
->setDescription(
28+
'Export a directory'
29+
)
30+
->addArgument(
31+
'key',
32+
InputArgument::REQUIRED,
33+
'Dir to export'
34+
)->addOption(
35+
'output',
36+
'o',
37+
InputOption::VALUE_REQUIRED,
38+
'json file to save dump'
39+
)->addOption(
40+
'format',
41+
'f',
42+
InputOption::VALUE_REQUIRED,
43+
'json file to save dump',
44+
self::FORMAT_JSON
45+
)->addOption(
46+
'server',
47+
's',
48+
InputOption::VALUE_REQUIRED,
49+
'Base url of etcd server',
50+
'http://127.0.0.1:2379'
51+
);
52+
}
53+
54+
public function execute(InputInterface $input, OutputInterface $output)
55+
{
56+
$key = $input->getArgument('key');
57+
$server = $input->getOption('server');
58+
59+
$client = new EtcdClient($server);
60+
$dirExporter = new DirectoryExporter($client);
61+
62+
switch ($input->getOption('format')) {
63+
case self::FORMAT_JSON:
64+
$result = $this->createJson($dirExporter, $key);
65+
break;
66+
case self::FORMAT_YAML:
67+
$result = $this->createYaml($dirExporter, $key);
68+
break;
69+
case self::FORMAT_DOTENV:
70+
$result = $this->createDotEnv($dirExporter, $key);
71+
break;
72+
default:
73+
throw new \RuntimeException('Unknown format: ' . $input->getOption('format'));
74+
}
75+
76+
$file = $input->getOption('output');
77+
if (!is_null($file)) {
78+
$fs = new Filesystem();
79+
$fs->dumpFile($file, $result . PHP_EOL);
80+
} else {
81+
$output->writeln($result);
82+
}
83+
}
84+
85+
86+
private function createJson(DirectoryExporter $directoryExporter, $key)
87+
{
88+
$data = $directoryExporter->exportArray($key);
89+
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
90+
}
91+
92+
93+
private function createYaml(DirectoryExporter $directoryExporter, $key)
94+
{
95+
$data = $directoryExporter->exportArray($key);
96+
return Yaml::dump($data);
97+
}
98+
99+
100+
private function createDotEnv(DirectoryExporter $directoryExporter, $key)
101+
{
102+
$data = $directoryExporter->exportKeyValuePairs($key, true);
103+
$result = [];
104+
foreach ($data as $k => $v) {
105+
$result[] = strtoupper(str_replace("/", "_", $k)) . '=' . $v;
106+
}
107+
return implode(PHP_EOL, $result);
108+
}
109+
}

src/DirectoryExporter.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
namespace LinkORB\Component\Etcd;
4+
5+
class DirectoryExporter
6+
{
7+
const PATH_SEPARATOR = '/';
8+
9+
/** @var Client */
10+
private $client;
11+
12+
public function __construct(Client $client)
13+
{
14+
$this->client = $client;
15+
}
16+
17+
18+
public function exportArray($directory)
19+
{
20+
$lsResult = $this->client->listDir($directory, true);
21+
$rootPath = $lsResult['node']['key'];
22+
23+
$kvList = $this->createKeyValuePairs($lsResult);
24+
25+
$result = [];
26+
if (count($kvList) === 1 && array_keys($kvList)[0] === $rootPath) {
27+
// $dir is property
28+
$parts = explode(self::PATH_SEPARATOR, $rootPath);
29+
$result[$parts[count($parts) - 1]] = $kvList[$rootPath];
30+
} else {
31+
foreach ($kvList as $k => $v) {
32+
$realKey = substr($k, strlen($rootPath));
33+
$parts = explode(self::PATH_SEPARATOR, $realKey);
34+
array_shift($parts); // remove first empty element
35+
$this->addToDepth($result, $parts, $v);
36+
}
37+
}
38+
return $result;
39+
}
40+
41+
42+
public function exportKeyValuePairs($dir, $recursive = true)
43+
{
44+
$lsResult = $this->client->listDir($dir, $recursive);
45+
$rootPath = $lsResult['node']['key'];
46+
47+
$kvList = $this->createKeyValuePairs($lsResult);
48+
49+
$result = [];
50+
if (count($kvList) === 1 && array_keys($kvList)[0] === $rootPath) {
51+
// $dir is property
52+
$parts = explode(self::PATH_SEPARATOR, $rootPath);
53+
$result[$parts[count($parts) - 1]] = $kvList[$rootPath];
54+
} else {
55+
foreach ($kvList as $key => $value) {
56+
$result[substr($key, strlen($rootPath) + 1)] = $value;
57+
}
58+
}
59+
return $result;
60+
}
61+
62+
63+
private function createKeyValuePairs($lsResult)
64+
{
65+
$result = [];
66+
$iterator = new \RecursiveArrayIterator($lsResult);
67+
$this->traverse($result, $iterator);
68+
ksort($result);
69+
return $result;
70+
}
71+
72+
73+
private function traverse(&$values, \RecursiveArrayIterator $iterator)
74+
{
75+
while ($iterator->valid()) {
76+
if ($iterator->hasChildren()) {
77+
$this->traverse($values, $iterator->getChildren());
78+
} else {
79+
$currentLevel = $iterator->getArrayCopy();
80+
if (array_key_exists('key', $currentLevel) && array_key_exists('value', $currentLevel)) {
81+
$values[$currentLevel['key']] = $currentLevel['value'];
82+
return;
83+
}
84+
}
85+
$iterator->next();
86+
}
87+
}
88+
89+
90+
private function addToDepth(&$array, $path, $value)
91+
{
92+
if (1 === count($path)) {
93+
$array[current($path)] = $value;
94+
} else {
95+
$current = array_shift($path);
96+
if (!array_key_exists($current, $array)) {
97+
$array[$current] = [];
98+
}
99+
$this->addToDepth($array[$current], $path, $value);
100+
}
101+
}
102+
}

tests/BaseTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace LinkORB\Tests\Component\Etcd;
4+
5+
use LinkORB\Component\Etcd\Client;
6+
use PHPUnit\Framework\TestCase;
7+
8+
abstract class BaseTest extends TestCase
9+
{
10+
/** @var Client */
11+
protected $client;
12+
13+
protected $dirname = '/phpunit_test';
14+
15+
16+
protected function setUp()
17+
{
18+
$this->client = new Client();
19+
$this->client->mkdir($this->dirname);
20+
$this->client->setRoot($this->dirname);
21+
}
22+
23+
protected function tearDown()
24+
{
25+
$this->client->setRoot('/');
26+
$this->client->rmdir($this->dirname, true);
27+
}
28+
}

tests/ClientTest.php

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,8 @@
22

33
namespace LinkORB\Tests\Component\Etcd;
44

5-
use LinkORB\Component\Etcd\Client;
6-
use PHPUnit\Framework\TestCase;
7-
8-
class ClientTest extends TestCase
5+
class ClientTest extends BaseTest
96
{
10-
/**
11-
* @var Client
12-
*/
13-
protected $client;
14-
15-
private $dirname = '/phpunit_test';
16-
17-
protected function setUp()
18-
{
19-
$this->client = new Client();
20-
$this->client->mkdir($this->dirname);
21-
$this->client->setRoot($this->dirname);
22-
}
23-
24-
protected function tearDown()
25-
{
26-
$this->client->setRoot('/');
27-
$this->client->rmdir($this->dirname, true);
28-
}
29-
307
/**
318
* @covers LinkORB\Component\Etcd\Client::getVersion
329
*/
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace LinkORB\Tests\Component\Etcd;
4+
5+
use GuzzleHttp\Exception\ClientException;
6+
use LinkORB\Component\Etcd\DirectoryExporter;
7+
8+
class DirectoryExporterArrayTest extends BaseTest
9+
{
10+
use FixtureTrait;
11+
12+
protected function setUp()
13+
{
14+
parent::setUp();
15+
$this->prepareFixture($this->client);
16+
}
17+
18+
private function getDirectoryExporter()
19+
{
20+
return new DirectoryExporter($this->client);
21+
}
22+
23+
24+
public function testFailOnNotExistingKey()
25+
{
26+
$this->expectException(ClientException::class);
27+
$this->expectExceptionCode(404);
28+
$this->getDirectoryExporter()->exportArray(__FUNCTION__);
29+
}
30+
31+
32+
public function testRootDir()
33+
{
34+
$res = $this->getDirectoryExporter()->exportArray('/');
35+
$this->assertEquals($this->getExpectedFullTreeAsArray(), $res);
36+
}
37+
38+
39+
public function testDir()
40+
{
41+
$res = $this->getDirectoryExporter()->exportArray('/dir');
42+
$this->assertEquals($this->getExpectedDirAsArray(), $res);
43+
}
44+
45+
46+
public function testSubDir()
47+
{
48+
$res = $this->getDirectoryExporter()->exportArray('/dir/sub2/');
49+
$this->assertEquals(
50+
[
51+
'f1' => 'vs2_1',
52+
'f2' => 'vs2_2',
53+
],
54+
$res
55+
);
56+
}
57+
58+
59+
public function testProperty()
60+
{
61+
$result = $this->getDirectoryExporter()->exportArray('/dir/sub2/f1');
62+
$this->assertEquals(
63+
[
64+
'f1' => 'vs2_1',
65+
],
66+
$result
67+
);
68+
}
69+
}

0 commit comments

Comments
 (0)