diff --git a/.gitignore b/.gitignore index aff427f..b1a7d81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /vendor/ /composer.lock .phpunit.result.cache -.env \ No newline at end of file +.env +.idea diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/digitalocean-api.iml b/.idea/digitalocean-api.iml deleted file mode 100644 index 3919d1b..0000000 --- a/.idea/digitalocean-api.iml +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 0bd959c..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml deleted file mode 100644 index 613c06f..0000000 --- a/.idea/php.xml +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/phpunit.xml b/.idea/phpunit.xml deleted file mode 100644 index 4f8104c..0000000 --- a/.idea/phpunit.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 225c719..629e639 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,93 @@ # Digital Ocean API for Laravel Framework +--- + +Digital Ocean API for Laravel Framework is a package created by Arayik Smbatyan ([@arayiksmbatyan](https://github.com/arayiksmbatyan)) from **Lionix** to make it easier to use Digital Ocean API in Laravel Framework. + +The package is not using any external libraries like DO PHP SDK, it uses general DO API, therefore it is very easy extendable. + +## Installation + +You can install the package via composer: + +```bash +composer require lionix/digitalocean +``` + +## Publishing the config file + +```bash +php artisan vendor:publish --provider="Lionix\DigitalOcean\DigitalOceanServiceProvider" --tag="config" +``` + +## API KEY + +Open your Digitalocean Account and go to API section. [Generate a new Personal Access Token](https://cloud.digitalocean.com/account/api/tokens/new?i=c1d240) with `write` access and add to your `.env` file. + +```apacheconf +DO_API_KEY=your_api_key +``` + +## Available Services + +- [Droplets](#droplets) +- [Droplet Actions](#droplet-actions) +- [Domains](#domains) +- [Snapshots](#snapshots) +- [Global Service](#global-service) +- [DO Snapshot Command](#do-snapshot-command) + +All the services can be used +by injecting the service into +your controller, +by using the `Digitalocean` facade or by using the service facade (e.g. `Droplets`). + + +## Droplets + +### Using via Service +```php +index(); + + return response()->json($droplets); + } +} +``` + +### Using via Facade + +```php +Droplets::list(); +``` + +### Using via Digitalocean Facade + +```php +Digitalocean::droplets()->list(); +``` + +### Available Methods + +- `list()` +- `store()` +- `show()` +- `destroy()` + +--- + +### Read full documentation in our [Docs](https://docs.lionix.io/) + diff --git a/composer.json b/composer.json index 5ce1cd5..8e954f4 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "lionix-team/digitalocean-api", + "name": "lionix/digitalocean", "description": "Digital Ocean API for Laravel Framework", "type": "library", "license": "MIT", @@ -7,6 +7,10 @@ { "name": "Arayik Smbatyan", "email": "arayiksmbatyan@gmail.com" + }, + { + "name": "Sergey Karakhanyan", + "email": "sergey@lionix.io" } ], "require": { @@ -15,7 +19,7 @@ }, "autoload": { "psr-4": { - "DigitaloceanApi\\" : "./src" + "Digitalocean\\" : "./src" } }, "autoload-dev": { @@ -26,11 +30,14 @@ "extra": { "laravel": { "providers": [ - "DigitaloceanApi\\Providers\\ConfigServiceProvider" + "Digitalocean\\Providers\\ConfigServiceProvider" ], "aliases": { - "Domains": "DigitaloceanApi\\Facades\\DomainFacade", - "Droplets": "DigitaloceanApi\\Facades\\DropletFacade" + "Domains": "Digitalocean\\Facades\\DomainFacade", + "Droplets": "Digitalocean\\Facades\\DropletsFacade", + "Snapshots": "Digitalocean\\Facades\\SnapshotFacade", + "DropletActions": "Digitalocean\\Facades\\DropletActions", + "Digitalocean": "Digitalocean\\Facades\\DigitaloceanFacade" } } }, diff --git a/config/digital-ocean.php b/config/digital-ocean.php index aee7f97..45c911c 100644 --- a/config/digital-ocean.php +++ b/config/digital-ocean.php @@ -1,9 +1,17 @@ env('TOKEN', ''), - 'api-urls' => [ - 'domains' => 'https://api.digitalocean.com/v2/domains', - 'droplets' => 'https://api.digitalocean.com/v2/droplets' + 'token' => env('DO_APP_TOKEN', ''), + 'base_url' => 'https://api.digitalocean.com/v2/', + 'dropletId' => env('DO_DROPLET_ID'), + + 'endpoints' => [ + 'domains' => 'domains', + 'droplets' => [ + 'index' => 'droplets', + 'snapshots' => 'droplets/:dropletId/snapshots', + 'actions' => 'droplets/:dropletId/actions', + ], + 'snapshots' => 'snapshots', ], -]; \ No newline at end of file +]; diff --git a/src/Commands/DOSnapshotCommand.php b/src/Commands/DOSnapshotCommand.php new file mode 100644 index 0000000..0657992 --- /dev/null +++ b/src/Commands/DOSnapshotCommand.php @@ -0,0 +1,43 @@ +option('dropletId') ?? config('digital-ocean.droplet_id'); + + if (!$dropletId) { + $dropletId = $this->ask('What is your droplet id?'); + } + + if ($this->option('dropOldSnapshots')) { + $snapshots = $snapshotService->list($dropletId); + + collect($snapshots['snapshots'])->each(fn($snapshot) => $snapshotService->destroy($snapshot['id'])); + } + + $response = $snapshotService->make((int)$dropletId, $this->option('name')); + + if ($response['status_code'] === 200 || $response['status_code'] === 201) { + $this->info('Snapshot for droplet ' . $dropletId . ' created successfully'); + } else { + $this->info('Something went wrong'); + } + + return 0; + } +} diff --git a/src/Facades/DigitaloceanFacade.php b/src/Facades/DigitaloceanFacade.php new file mode 100644 index 0000000..20a40ac --- /dev/null +++ b/src/Facades/DigitaloceanFacade.php @@ -0,0 +1,14 @@ +publishes([ __DIR__.'/../../config/digital-ocean.php' => config_path('digital-ocean.php'), ]); + + if ($this->app->runningInConsole()) { + $this->commands([ + DOSnapshotCommand::class, + ]); + } } /** @@ -24,12 +37,41 @@ public function boot() * * @return void */ - public function register() + public function register(): void { $this->mergeConfigFrom( __DIR__.'/../../config/digital-ocean.php', 'digital-ocean' ); - $this->app->bind(DomainService::class); + $this->app->singleton(DigitaloceanApi::class, function ($app) { + $client = new Client([ + 'base_uri' => config('digital-ocean.base_url'), + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '. config('digital-ocean.token'), + ]]); + + return new DigitaloceanApi($client); + }); + + $this->app->singleton(DomainService::class, function ($app) { + return new DomainService($app->make(DigitaloceanApi::class)); + }); + + $this->app->singleton(DropletsService::class, function ($app) { + return new DropletsService($app->make(DigitaloceanApi::class)); + }); + + $this->app->singleton(SnapshotService::class, function ($app) { + return new SnapshotService($app->make(DigitaloceanApi::class)); + }); + + $this->app->singleton(DropletActionService::class, function ($app) { + return new DropletActionService($app->make(DigitaloceanApi::class)); + }); + + $this->app->singleton(DigitaloceanService::class, function ($app) { + return new DigitaloceanService($app->make(DigitaloceanApi::class)); + }); } -} \ No newline at end of file +} diff --git a/src/Services/DigitaloceanApi.php b/src/Services/DigitaloceanApi.php new file mode 100644 index 0000000..b83a135 --- /dev/null +++ b/src/Services/DigitaloceanApi.php @@ -0,0 +1,42 @@ +client->request($method, $uri, ['json' => $params]); + + if($method !== 'DELETE') { + $response = \json_decode($res->getBody(), true, 512, JSON_THROW_ON_ERROR); + } + + $response['status_code'] = $res->getStatusCode(); + + return $response; + + } catch (\GuzzleHttp\Exception\RequestException $exception) { + $error = \json_decode($exception->getResponse()->getBody(), true, 512, JSON_THROW_ON_ERROR); + $error['status_code'] = $exception->getResponse()->getStatusCode(); + + return $error; + } + } +} diff --git a/src/Services/DigitaloceanService.php b/src/Services/DigitaloceanService.php new file mode 100644 index 0000000..b0a9c15 --- /dev/null +++ b/src/Services/DigitaloceanService.php @@ -0,0 +1,44 @@ +digitaloceanApi->send($method, $uri, $params); + } + + public function droplets(): mixed + { + return app(DropletsService::class); + } + + public function domains(): mixed + { + return app(DomainService::class); + } + + public function dropletActions() + { + return app(DropletActionService::class); + } + + public function snapshots() + { + return app(SnapshotService::class); + } +} diff --git a/src/Services/DomainService.php b/src/Services/DomainService.php index 3946fd4..3b4960f 100644 --- a/src/Services/DomainService.php +++ b/src/Services/DomainService.php @@ -1,39 +1,31 @@ domainsApiUrl = config('digital-ocean.api-urls.domains'); - $this->sendRequestService = $sendRequestService; + $this->domainsApiUrl = config('digital-ocean.endpoints.domains'); } /** * @return mixed|\stdClass - * - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \GuzzleHttp\Exception\GuzzleException|\JsonException */ - public function index() + public function list(): mixed { - return $this->sendRequestService->send('GET', $this->domainsApiUrl); + return $this->digitaloceanApi->send('GET', $this->domainsApiUrl); } /** @@ -41,17 +33,17 @@ public function index() * * @return \Illuminate\Support\MessageBag|mixed|\stdClass * - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \GuzzleHttp\Exception\GuzzleException|\JsonException */ - public function store(array $params) + public function store(array $params): mixed { $validator = Validator::make($params, $this->getStoreRules()); - if($validator->fails()) { + if ($validator->fails()) { return $validator->errors(); } - return $this->sendRequestService->send('POST', $this->domainsApiUrl, ['json' => $params]); + return $this->digitaloceanApi->send('POST', $this->domainsApiUrl, $params); } /** @@ -59,33 +51,32 @@ public function store(array $params) * * @return mixed|\stdClass * - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \GuzzleHttp\Exception\GuzzleException|\JsonException */ - public function show(string $name) + public function show(string $name): mixed { - return $this->sendRequestService->send('GET', "{$this->domainsApiUrl}/{$name}"); + return $this->digitaloceanApi->send('GET', "{$this->domainsApiUrl}/{$name}"); } /** * @param string $name * * @return mixed|\stdClass - - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \GuzzleHttp\Exception\GuzzleException|\JsonException */ - public function destroy(string $name) + public function destroy(string $name): mixed { - return $this->sendRequestService->send('DELETE', "{$this->domainsApiUrl}/${name}"); + return $this->digitaloceanApi->send('DELETE', "{$this->domainsApiUrl}/${name}"); } /** * @return string[] */ - private function getStoreRules() + private function getStoreRules(): array { return [ 'name' => 'required|string|max:255', 'ip_address' => 'nullable|string', ]; } -} \ No newline at end of file +} diff --git a/src/Services/DropletActionService.php b/src/Services/DropletActionService.php new file mode 100644 index 0000000..6240e22 --- /dev/null +++ b/src/Services/DropletActionService.php @@ -0,0 +1,28 @@ +digitaloceanApi->send('POST', + str_replace(':dropletId', $dropletId, config('digital-ocean.endpoints.droplets.actions')), + [ + 'type' => $type, + ...$params, + ] + ); + } +} diff --git a/src/Services/DropletService.php b/src/Services/DropletService.php deleted file mode 100644 index df4f323..0000000 --- a/src/Services/DropletService.php +++ /dev/null @@ -1,113 +0,0 @@ -domainsApiUrl = config('digital-ocean.api-urls.droplets'); - $this->sendRequestService = $sendRequestService; - } - - /** - * @param int $perPage - * @param int $page - * - * @return mixed|\stdClass - * - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function index( - int $perPage = 20, - int $page = 1 - ) { - $repuestParams = [ - 'json' =>[ - 'per_page' => $perPage, - 'page' => $page, - ], - ]; - - return $this->sendRequestService->send('GET', $this->domainsApiUrl, $repuestParams); - } - - /** - * @param array $params - * - * @return \Illuminate\Support\MessageBag|mixed|\stdClass - * - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function store(array $params) - { - $validator = Validator::make($params, $this->getStoreRules()); - - if($validator->fails()) { - return $validator->errors(); - } - - return $this->sendRequestService->send('POST', $this->domainsApiUrl, ['json' => $params]); - } - - /** - * @param int $dropletId - * - * @return mixed|\stdClass - * - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function show(int $dropletId) - { - return $this->sendRequestService->send('GET', "{$this->domainsApiUrl}/{$dropletId}"); - } - - /** - * @param int $dropletId - * - * @return mixed|\stdClass - * - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function destroy(int $dropletId) - { - return $this->sendRequestService->send('DELETE', "{$this->domainsApiUrl}/{$dropletId}"); - } - - /** - * @return string[] - */ - private function getStoreRules() - { - return [ - 'name' => 'required|string', - 'region' => 'required|string', - 'size' => 'required|string', - 'image' => 'required|regex:/^[a-zA-Z0-9\s]+$/', - 'ssh_keys' => 'nullable|array', - 'backups' => 'nullable|boolean', - 'ipv6' => 'nullable|boolean', - 'monitoring' => 'nullable|boolean', - 'tags' => 'nullable|array', - 'user_data' => 'nullable|string', - 'vpc_uuid' => 'nullable|string', - 'with_droplet_agent' => 'nullable|boolean', - ]; - } -} \ No newline at end of file diff --git a/src/Services/DropletsService.php b/src/Services/DropletsService.php new file mode 100644 index 0000000..6d19e39 --- /dev/null +++ b/src/Services/DropletsService.php @@ -0,0 +1,105 @@ +domainsApiUrl = config('digital-ocean.endpoints.droplets.index'); + } + + /** + * @param int $perPage + * @param int $page + * + * @return mixed|\stdClass + * + * @throws \GuzzleHttp\Exception\GuzzleException|\JsonException + */ + public function list( + int $perPage = 20, + int $page = 1 + ): mixed + { + $params = [ + 'per_page' => $perPage, + 'page' => $page, + ]; + + return $this->digitaloceanApi->send('GET', $this->domainsApiUrl, $params); + } + + /** + * @param array $params + * + * @return \Illuminate\Support\MessageBag|mixed|\stdClass + * + * @throws \GuzzleHttp\Exception\GuzzleException|\JsonException + */ + public function store(array $params): mixed + { + $validator = Validator::make($params, $this->getStoreRules()); + + if($validator->fails()) { + return $validator->errors(); + } + + return $this->digitaloceanApi->send('POST', $this->domainsApiUrl, $params); + } + + /** + * @param int $dropletId + * + * @return mixed|\stdClass + * + * @throws \GuzzleHttp\Exception\GuzzleException|\JsonException + */ + public function show(int $dropletId): mixed + { + return $this->digitaloceanApi->send('GET', "{$this->domainsApiUrl}/{$dropletId}"); + } + + /** + * @param int $dropletId + * + * @return mixed|\stdClass + * + * @throws \GuzzleHttp\Exception\GuzzleException|\JsonException + */ + public function destroy(int $dropletId): mixed + { + return $this->digitaloceanApi->send('DELETE', "{$this->domainsApiUrl}/{$dropletId}"); + } + + /** + * @return string[] + */ + private function getStoreRules(): array + { + return [ + 'name' => 'required|string', + 'region' => 'required|string', + 'size' => 'required|string', + 'image' => 'required|regex:/^[a-zA-Z0-9\s]+$/', + 'ssh_keys' => 'nullable|array', + 'backups' => 'nullable|boolean', + 'ipv6' => 'nullable|boolean', + 'monitoring' => 'nullable|boolean', + 'tags' => 'nullable|array', + 'user_data' => 'nullable|string', + 'vpc_uuid' => 'nullable|string', + 'with_droplet_agent' => 'nullable|boolean', + ]; + } +} diff --git a/src/Services/SendRequestService.php b/src/Services/SendRequestService.php deleted file mode 100644 index adf992e..0000000 --- a/src/Services/SendRequestService.php +++ /dev/null @@ -1,55 +0,0 @@ -client = new Client([ - 'headers' => [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer '. config('digital-ocean.token'), - ]]); - } - - /** - * @param $method - * @param $uri - * @param $params - * - * @return mixed|\stdClass - * - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function send($method, $uri, $params = []) - { - try { - $res = $this->client->request($method, $uri, $params); - - if($method !== 'DELETE') { - $response = \json_decode($res->getBody()); - $response->status_code = $res->getStatusCode(); - - return $response; - } - $response = new \stdClass(); - $response->status_code = $res->getStatusCode(); - - return $response; - - } catch (\GuzzleHttp\Exception\RequestException $exception) { - $error = \json_decode($exception->getResponse()->getBody()); - $error->status_code = $exception->getResponse()->getStatusCode(); - - return $error; - } - } -} \ No newline at end of file diff --git a/src/Services/SnapshotService.php b/src/Services/SnapshotService.php new file mode 100644 index 0000000..49df758 --- /dev/null +++ b/src/Services/SnapshotService.php @@ -0,0 +1,70 @@ +snapshotsApiUrl = config('digital-ocean.endpoints.snapshots'); + } + + /** + * @return mixed|\stdClass + * @throws \GuzzleHttp\Exception\GuzzleException|\JsonException + */ + public function list(int $dropletId): mixed + { + return $this->digitaloceanApi->send('GET', + str_replace(':dropletId', $dropletId, config('digital-ocean.endpoints.droplets.snapshots')) + ); + } + + /** + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \JsonException + */ + public function make(int $dropletId, string $name = null) + { + return $this->digitaloceanApi->send('POST', + str_replace(':dropletId', $dropletId, config('digital-ocean.endpoints.droplets.actions')), + [ + 'type' => 'snapshot', + 'name' => ($name ?: $dropletId) . '-' . now()->toDateTimeString(), + ] + ); + } + + /** + * @param string $snapshotId + * + * @return mixed|\stdClass + * + * @throws \GuzzleHttp\Exception\GuzzleException|\JsonException + */ + public function show(string $snapshotId): mixed + { + return $this->digitaloceanApi->send('GET', "{$this->snapshotsApiUrl}/{$snapshotId}"); + } + + /** + * @param string $snapshotId + * + * @return mixed|\stdClass + * @throws \GuzzleHttp\Exception\GuzzleException|\JsonException + */ + public function destroy(string $snapshotId): mixed + { + return $this->digitaloceanApi->send('DELETE', "{$this->snapshotsApiUrl}/{$snapshotId}"); + } +} diff --git a/tests/Feature/Domains/TestDomainService.php b/tests/Feature/Domains/TestDomainService.php index f5f7eb6..b951b53 100644 --- a/tests/Feature/Domains/TestDomainService.php +++ b/tests/Feature/Domains/TestDomainService.php @@ -3,7 +3,7 @@ namespace Tests\Feature\Domains; use Tests\TestCase; -use DigitaloceanApi\Services\DomainService; +use Digitalocean\Services\DomainService; class TestDomainService extends TestCase { @@ -18,7 +18,7 @@ public function setUp(): void public function testIndex() { - $result = $this->service->index(); + $result = $this->service->list(); file_put_contents("tests/logs/DomainServiceLogs.log", json_encode($result).PHP_EOL, FILE_APPEND | LOCK_EX); } @@ -39,4 +39,4 @@ public function testDestroy() $result = $this->service->destroy('digital-ocean-package.io'); file_put_contents("tests/logs/DomainServiceLogs.log", json_encode($result).PHP_EOL, FILE_APPEND | LOCK_EX); } -} \ No newline at end of file +} diff --git a/tests/Feature/Droplets/TestDropletService.php b/tests/Feature/Droplets/TestDropletService.php index 8686789..f4246a0 100644 --- a/tests/Feature/Droplets/TestDropletService.php +++ b/tests/Feature/Droplets/TestDropletService.php @@ -3,7 +3,7 @@ namespace Tests\Feature\Droplets; use Tests\TestCase; -use DigitaloceanApi\Services\DropletService; +use Digitalocean\Services\DropletsService; class TestDropletService extends TestCase { @@ -12,12 +12,12 @@ class TestDropletService extends TestCase public function setUp(): void { parent::setUp(); - $this->service = $this->app->make(DropletService::class); + $this->service = $this->app->make(DropletsService::class); } public function testIndex() { - $result = $this->service->index(); + $result = $this->service->list(); file_put_contents("tests/logs/DropletServiceLog.log", json_encode($result).PHP_EOL, FILE_APPEND | LOCK_EX); } -} \ No newline at end of file +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 33ab6f6..951f2a5 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,7 @@ namespace Tests; -use DigitaloceanApi\Providers\ConfigServiceProvider; +use Digitalocean\Providers\ConfigServiceProvider; use Orchestra\Testbench\TestCase as OrchestraTestCase; class TestCase extends OrchestraTestCase @@ -23,4 +23,4 @@ protected function getEnvironmentSetUp($app) { // perform environment setup } -} \ No newline at end of file +}