diff --git a/.gitignore b/.gitignore index 5b006df..30db829 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,4 @@ fabric.properties /accounts.json /.php-cs-fixer.cache /tmp +/do diff --git a/README.md b/README.md new file mode 100644 index 0000000..e51381b --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Digitalocean API shell +Console utility for working with DigitalOcean API + +### Requirements +* PHP 7.4+ +* [Composer](https://getcomposer.org) +* PHP [Curl](https://www.php.net/manual/book.curl.php) extension +* PHP [GMP](https://www.php.net/manual/book.gmp.php) extension +* PHP [JSON](https://www.php.net/manual/book.json.php) extension + +### Installation +* Run `composer create-project grayfolk/digitalocean-api-shell`. +* Go to `digitalocean-api-shell` folder and edit `accounts.json`. This is a simple [JSON](https://www.json.org) file with `"username" : "API Key"` records. You can use whatever name is convenient for you (but it shoud be unique) and real DigitalOcean API key. You can obtain API key in your [DigitalOcean Account - API - Personal access tokens - Generate New Token](https://cloud.digitalocean.com/account/api/tokens/new). + +### Running +Go to `digitalocean-api-shell` folder and run `./do`. diff --git a/composer.json b/composer.json index 40bff47..5aa9d0d 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "email": "grayfolk@gmail.com" } ], - "minimum-stability": "dev", + "minimum-stability": "stable", "autoload": { "psr-4": { "App\\": "src/" @@ -35,7 +35,8 @@ }, "scripts": { "post-install-cmd": [ - "chmod +x do.sh", + "ln -sf src/digitalocean-api-shell.sh do", + "chmod +x do", "mkdir -m=777 -p tmp", "cp accounts.json.example accounts.json" ], diff --git a/composer.lock b/composer.lock index ee5057a..9c1a637 100644 --- a/composer.lock +++ b/composer.lock @@ -303,10 +303,6 @@ "rest", "web service" ], - "support": { - "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.3.0" - }, "funding": [ { "url": "https://github.com/GrahamCampbell", @@ -329,16 +325,16 @@ }, { "name": "guzzlehttp/promises", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" + "reference": "136a635e2b4a49b9d79e9c8fee267ffb257fdba0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", - "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "url": "https://api.github.com/repos/guzzle/promises/zipball/136a635e2b4a49b9d79e9c8fee267ffb257fdba0", + "reference": "136a635e2b4a49b9d79e9c8fee267ffb257fdba0", "shasum": "" }, "require": { @@ -350,7 +346,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -366,34 +362,59 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle promises library", "keywords": [ "promise" ], - "support": { - "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.4.1" - }, - "time": "2021-03-07T09:25:29+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2021-10-07T13:05:22+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "1dc8d9cba3897165e16d12bb13d813afb1eb3fe7" + "reference": "089edd38f5b8abba6cb01567c2a8aaa47cec4c72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/1dc8d9cba3897165e16d12bb13d813afb1eb3fe7", - "reference": "1dc8d9cba3897165e16d12bb13d813afb1eb3fe7", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/089edd38f5b8abba6cb01567c2a8aaa47cec4c72", + "reference": "089edd38f5b8abba6cb01567c2a8aaa47cec4c72", "shasum": "" }, "require": { @@ -417,7 +438,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -430,13 +451,34 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, { "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", "homepage": "https://github.com/Tobion" }, { @@ -456,11 +498,21 @@ "uri", "url" ], - "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.0.0" - }, - "time": "2021-06-30T20:03:07+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2021-10-06T17:43:30+00:00" }, { "name": "http-interop/http-factory-guzzle", @@ -780,10 +832,6 @@ "client", "http" ], - "support": { - "issues": "https://github.com/php-http/httplug/issues", - "source": "https://github.com/php-http/httplug/tree/master" - }, "time": "2020-07-13T15:43:23+00:00" }, { @@ -908,10 +956,6 @@ "stream", "uri" ], - "support": { - "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/master" - }, "time": "2015-12-19T14:08:53+00:00" }, { @@ -965,10 +1009,6 @@ "keywords": [ "promise" ], - "support": { - "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.1.0" - }, "time": "2020-07-07T09:29:14+00:00" }, { @@ -1018,9 +1058,6 @@ "psr", "psr-18" ], - "support": { - "source": "https://github.com/php-fig/http-client/tree/master" - }, "time": "2020-06-29T06:28:15+00:00" }, { @@ -1073,9 +1110,6 @@ "request", "response" ], - "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" - }, "time": "2019-04-30T12:38:16+00:00" }, { @@ -1126,9 +1160,6 @@ "request", "response" ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/master" - }, "time": "2016-08-06T14:39:51+00:00" }, { @@ -1368,9 +1399,6 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1437,9 +1465,6 @@ "configuration", "options" ], - "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.3.7" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1516,9 +1541,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.23.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1599,9 +1621,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1688,10 +1707,6 @@ "digitalocean", "vps" ], - "support": { - "issues": "https://github.com/DigitalOceanPHP/Client/issues", - "source": "https://github.com/DigitalOceanPHP/Client/tree/4.3.0" - }, "funding": [ { "url": "https://github.com/GrahamCampbell", @@ -1882,11 +1897,6 @@ "validation", "versioning" ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.2.5" - }, "funding": [ { "url": "https://packagist.com", @@ -2033,10 +2043,6 @@ "docblock", "parser" ], - "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.13.2" - }, "time": "2021-08-05T19:00:23+00:00" }, { @@ -2099,10 +2105,6 @@ "parser", "php" ], - "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.1" - }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -2121,16 +2123,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.1.0", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "cf4cedb9e8991c2daa94a756176d81bf487e4c4b" + "reference": "13ae36a76b6e329e44ca3cafaa784ea02db9ff14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/cf4cedb9e8991c2daa94a756176d81bf487e4c4b", - "reference": "cf4cedb9e8991c2daa94a756176d81bf487e4c4b", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/13ae36a76b6e329e44ca3cafaa784ea02db9ff14", + "reference": "13ae36a76b6e329e44ca3cafaa784ea02db9ff14", "shasum": "" }, "require": { @@ -2139,7 +2141,7 @@ "doctrine/annotations": "^1.12", "ext-json": "*", "ext-tokenizer": "*", - "php": "^7.1.3 || ^8.0", + "php": "^7.2 || ^8.0", "php-cs-fixer/diff": "^2.0", "symfony/console": "^4.4.20 || ^5.1.3", "symfony/event-dispatcher": "^4.4.20 || ^5.0", @@ -2147,13 +2149,14 @@ "symfony/finder": "^4.4.20 || ^5.0", "symfony/options-resolver": "^4.4.20 || ^5.0", "symfony/polyfill-php72": "^1.23", + "symfony/polyfill-php80": "^1.23", "symfony/polyfill-php81": "^1.23", "symfony/process": "^4.4.20 || ^5.0", "symfony/stopwatch": "^4.4.20 || ^5.0" }, "require-dev": { "justinrainbow/json-schema": "^5.2", - "keradus/cli-executor": "^1.4", + "keradus/cli-executor": "^1.5", "mikey179/vfsstream": "^1.6.8", "php-coveralls/php-coveralls": "^2.4.3", "php-cs-fixer/accessible-object": "^1.1", @@ -2196,17 +2199,13 @@ } ], "description": "A tool to automatically fix PHP code style", - "support": { - "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.1.0" - }, "funding": [ { "url": "https://github.com/keradus", "type": "github" } ], - "time": "2021-08-29T20:16:20+00:00" + "time": "2021-10-05T08:12:17+00:00" }, { "name": "php-cs-fixer/diff", @@ -2351,10 +2350,6 @@ "container-interop", "psr" ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.1" - }, "time": "2021-03-05T17:36:06+00:00" }, { @@ -2401,10 +2396,6 @@ "psr", "psr-14" ], - "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" - }, "time": "2019-01-08T18:20:26+00:00" }, { @@ -2487,9 +2478,6 @@ "console", "terminal" ], - "support": { - "source": "https://github.com/symfony/console/tree/v5.3.7" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2572,9 +2560,6 @@ ], "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.3.7" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2651,9 +2636,6 @@ "interoperability", "standards" ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.4.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2714,9 +2696,6 @@ ], "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.3.4" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2776,9 +2755,6 @@ ], "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v5.3.7" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2855,9 +2831,6 @@ "polyfill", "portable" ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2936,9 +2909,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.23.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3020,9 +2990,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3100,9 +3067,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3176,9 +3140,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.23.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3255,9 +3216,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.23.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3317,9 +3275,6 @@ ], "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v5.3.7" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3396,9 +3351,6 @@ "interoperability", "standards" ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3458,9 +3410,6 @@ ], "description": "Provides a way to profile code", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.3.4" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3541,9 +3490,6 @@ "utf-8", "utf8" ], - "support": { - "source": "https://github.com/symfony/string/tree/v5.3.7" - }, "funding": [ { "url": "https://symfony.com/sponsor", diff --git a/src/App.php b/src/App.php index 80a143f..09f08d6 100644 --- a/src/App.php +++ b/src/App.php @@ -15,9 +15,12 @@ /** * @author grayfolk + * @version 1.0.2 */ class App { + public const VERSION = '1.0.2'; + /** * Available API actions. */ @@ -32,18 +35,13 @@ class App /** * @var string */ - public string $account; + public ?string $account = null; /** * @var array */ public array $accounts = []; - /** - * @var string - */ - public string $action; - /** * @var array|false|false[]|string[]|null */ @@ -68,13 +66,26 @@ public function __construct() { $this->climate = new CLImate(); - $this->climate->clear(); + $this->drawHeader(); $this->options = getopt('wf::'); $this->isDryRun = !isset($this->options['w']); } + public function drawHeader(): void + { + $this->climate->clear(); + + $this->climate->bold()->green()->flank(sprintf('DigitalOcean API Console wrapper. v%s', self::VERSION)); + + if ($this->account) { + $this->climate->bold()->green()->flank(sprintf('You\'re logged as %s', $this->account)); + } + + $this->climate->br(); + } + /** * @param mixed $excludes * @param mixed $message @@ -109,19 +120,22 @@ public function selectAccount($message = 'Select DigitalOcean account:', $exclud throw new Exception('No accounts found in accounts.json'); } - $this->account = $this->radio($message, array_keys($accounts)); + $account = $this->radio($message, array_keys($accounts)); if ($forceAuth) { + $this->account = $account; + $this->auth($this->accounts[$this->account]); } - return $this->account; + return $account; } /** * @throws Exception + * @param mixed $drawHeader */ - public function auth(string $apiKey): void + public function auth(string $apiKey, $drawHeader = true): void { if (!$this->client) { $this->client = new Client(); @@ -130,6 +144,10 @@ public function auth(string $apiKey): void $this->client->authenticate($apiKey); AccountAction::getInstance($this)->getInfo(); + + if ($drawHeader) { + $this->drawHeader(); + } } /** @@ -139,7 +157,7 @@ public function selectAction(): void { $action = $this->radio('Select what do you want:', array_keys(self::ACTIONS)); - /** @var $className AccountAction|CacheAction|ChangeAccountAction|DomainAction */ + /** @var $className AccountAction|CacheAction|ChangeAccountAction|DomainAction|ExitAction */ $className = self::ACTIONS[$action]; try { @@ -151,6 +169,8 @@ public function selectAction(): void } } + $this->drawHeader(); + $className::getInstance($this)->run(); } catch (Exception $e) { throw new Exception($e->getMessage()); diff --git a/src/actions/AccountAction.php b/src/actions/AccountAction.php index c967e0f..023258c 100644 --- a/src/actions/AccountAction.php +++ b/src/actions/AccountAction.php @@ -85,6 +85,9 @@ public function run(): void } } + /** + * @throws Exception + */ public function getInfo(): void { if (!$this->info) { diff --git a/src/actions/DomainAction.php b/src/actions/DomainAction.php index 194de87..d9e577c 100644 --- a/src/actions/DomainAction.php +++ b/src/actions/DomainAction.php @@ -92,20 +92,6 @@ public function run(): void $this->domain = $this->app->radio('Select domain:', ArrayHelper::getColumn($this->domains, 'name')); - try { - // Save zone file - $dir = sprintf('./tmp/%s', date('Y-m-d')); - if (!file_exists($dir) || !is_dir($dir)) { - mkdir($dir, 0777, true); - } - $file = sprintf('%s/%s-%s.conf', $dir, $this->domain, microtime(true)); - $domain = $this->app->client->domain()->getByName($this->domain); - file_put_contents($file, $domain->zoneFile); - $this->app->climate->info("Zone file backuped: {$file}"); - } catch (ExceptionInterface $e) { - throw new Exception($e->getMessage()); - } - if (!$this->domainRecords) { try { $this->domainRecords = $this->app->client->domainRecord()->getAll($this->domain); @@ -135,6 +121,27 @@ public function run(): void $ip = $this->app->radio('Select domain ip (A or AAAA):', $ips); + if (!$this->app->climate->confirm('Continue?')->confirmed()) { + $this->app->drawHeader(); + $this->app->selectAction(); + + return; + } + + try { + // Save zone file + $dir = sprintf('./tmp/%s', date('Y-m-d')); + if (!file_exists($dir) || !is_dir($dir)) { + mkdir($dir, 0777, true); + } + $file = sprintf('%s/%s-%s.conf', $dir, $this->domain, microtime(true)); + $domain = $this->app->client->domain()->getByName($this->domain); + file_put_contents($file, $domain->zoneFile); + $this->app->climate->info("Zone file backuped: {$file}"); + } catch (ExceptionInterface $e) { + throw new Exception($e->getMessage()); + } + try { if (!$this->app->isDryRun) { $this->app->client->domain()->remove($this->domain); @@ -143,7 +150,7 @@ public function run(): void throw new Exception($e->getMessage()); } - $this->app->auth($this->app->accounts[$this->accountTo]); + $this->app->auth($this->app->accounts[$this->accountTo], false); try { if (!$this->app->isDryRun) { @@ -220,6 +227,8 @@ public function run(): void } } + $this->app->climate->input('Press Enter to continue...')->prompt(); + try { // Restore first account $this->app->auth($this->app->accounts[$this->accountFrom]); diff --git a/do.sh b/src/digitalocean-api-shell.sh similarity index 60% rename from do.sh rename to src/digitalocean-api-shell.sh index 4f57b2e..9e78f9c 100644 --- a/do.sh +++ b/src/digitalocean-api-shell.sh @@ -10,8 +10,8 @@ require_once('vendor/autoload.php'); $app = new App(); try { - $app->selectAccount(); - $app->selectAction(); + $app->selectAccount(); + $app->selectAction(); } catch (Exception $e) { - return $app->climate->error($e->getMessage()); + return $app->climate->error($e->getMessage()); }