├── .gitignore ├── MAINTAINERS.md ├── src ├── Exception │ ├── DkronException.php │ ├── DkronResponseException.php │ └── DkronNoAvailableServersException.php ├── Models │ ├── Status.php │ ├── Execution.php │ ├── Member.php │ └── Job.php ├── Endpoints.php └── Api.php ├── .travis.yml ├── CONTRIBUTING.md ├── composer.json ├── LICENSE.md ├── README.md └── tests ├── Models └── JobTest.php ├── EndpointsTest.php └── ApiTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | composer.lock 3 | composer-test.lock 4 | vendor/ 5 | .idea 6 | .DS_STORE -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Maksim Naumov 2 | Sardorbek Pulatov 3 | Yuriy Khabarov 4 | -------------------------------------------------------------------------------- /src/Exception/DkronException.php: -------------------------------------------------------------------------------- 1 | agent = $agent; 28 | $this->serf = $serf; 29 | $this->tags = $tags; 30 | } 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function getAgent(): array 36 | { 37 | return $this->agent; 38 | } 39 | 40 | /** 41 | * @return array 42 | */ 43 | public function getSerf(): array 44 | { 45 | return $this->serf; 46 | } 47 | 48 | /** 49 | * @return array 50 | */ 51 | public function getTags(): array 52 | { 53 | return $this->tags; 54 | } 55 | 56 | 57 | public function jsonSerialize() 58 | { 59 | return [ 60 | 'agent' => $this->agent, 61 | 'serf' => $this->serf, 62 | 'tags' => $this->tags, 63 | ]; 64 | } 65 | 66 | /** 67 | * @param array $data 68 | * @return Status 69 | */ 70 | public static function createFromArray(array $data): self 71 | { 72 | return new static( 73 | $data['agent'] ?? null, 74 | $data['serf'] ?? null, 75 | $data['tags'] ?? null 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Models/Execution.php: -------------------------------------------------------------------------------- 1 | jobName = $jobName; 43 | $this->startedAt = $startedAt; 44 | $this->finishedAt = $finishedAt; 45 | $this->success = $success; 46 | $this->output = $output; 47 | $this->nodeName = $nodeName; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getJobName(): string 54 | { 55 | return $this->jobName; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getStartedAt(): string 62 | { 63 | return $this->startedAt; 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getFinishedAt(): string 70 | { 71 | return $this->finishedAt; 72 | } 73 | 74 | /** 75 | * @return string 76 | */ 77 | public function getOutput(): string 78 | { 79 | return $this->output; 80 | } 81 | 82 | /** 83 | * @return string 84 | */ 85 | public function getNodeName(): string 86 | { 87 | return $this->nodeName; 88 | } 89 | 90 | /** 91 | * @return bool 92 | */ 93 | public function isSuccess(): bool 94 | { 95 | return $this->success; 96 | } 97 | 98 | public function jsonSerialize() 99 | { 100 | return [ 101 | 'job_name' => $this->jobName, 102 | 'started_at' => $this->startedAt, 103 | 'finished_at' => $this->finishedAt, 104 | 'success' => $this->success, 105 | 'output' => $this->output, 106 | 'node_name' => $this->nodeName, 107 | ]; 108 | } 109 | 110 | /** 111 | * @param array $data 112 | * @return Execution 113 | */ 114 | public static function createFromArray(array $data): self 115 | { 116 | return new static( 117 | $data['job_name'] ?? null, 118 | $data['started_at'] ?? null, 119 | $data['finished_at'] ?? null, 120 | $data['success'] ?? null, 121 | $data['output'] ?? null, 122 | $data['node_name'] ?? null 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/gromo/dkron-php-adapter.svg?branch=master)](https://travis-ci.com/gromo/dkron-php-adapter) 2 | [![PHP](https://img.shields.io/badge/PHP-%5E7.0-blue.svg)](https://packagist.org/packages/gromo/dkron-php-adapter) 3 | [![Dkron version](https://img.shields.io/badge/Dkron-v0.10.0-green.svg)](https://github.com/victorcoder/dkron/releases/tag/v0.10.0) 4 | 5 | 6 | 7 | # Dkron PHP Adapter 8 | 9 | Adapter to communicate with [Dkron](https://dkron.io). 10 | 11 | Please read [Dkron API](https://dkron.io/usage/api/) for usage details 12 | 13 | ## Install: 14 | - add `"gromo/dkron-php-adapter": "dev-master"` to your project `composer.json` 15 | - run `composer install` 16 | 17 | ## Use: 18 | ```php 19 | // connect to single ip 20 | $api = new \Dkron\Api('http://192.168.0.1:8080'); 21 | 22 | // get status 23 | $status = $api->getStatus(); 24 | 25 | // get all jobs 26 | $jobs = $api->getJobs(); 27 | 28 | // create & save job 29 | $newJob = new \Dkron\Models\Job('my-job', '@every 5m'); 30 | $newJob->setExecutor('shell'); 31 | $newJob->setExecutorConfig([ 32 | 'command' => 'ls -la /' 33 | ]); 34 | $api->saveJob($newJob); 35 | 36 | // create job from parsed json 37 | $newJobFromArray = \Dkron\Models\Job::createFromArray([ 38 | 'name' => 'job name', 39 | 'schedule' => 'job schedule', 40 | 'executor' => 'shell', 41 | 'executor_config' => [ 42 | 'command' => 'ls -la /tmp', 43 | ], 44 | // other parameters 45 | ]); 46 | 47 | // get job data as json string 48 | $json = json_encode($newJobFromArray); 49 | 50 | // get job by name 51 | $existingJob = $api->getJob('my-job'); 52 | 53 | // run job by name 54 | $api->runJob($existingJob->getName()); 55 | 56 | // get job executions 57 | $executions = $api->getJobExecutions($existingJob->getName()); 58 | 59 | // delete job by name 60 | $api->deleteJob($existingJob->getName()); 61 | 62 | // get current leader node 63 | $leader = $api->getLeader(); 64 | 65 | // get all nodes 66 | $members = $api->getMembers(); 67 | 68 | // force current node to leave cluster 69 | $api->leave(); 70 | 71 | 72 | // connect to multiple servers with round-robin requests 73 | $mApi = new \Dkron\Api(['http://192.168.0.1:8080', 'http://192.168.0.2:8080']); 74 | 75 | // force selected node to leave cluster 76 | $mApi->leave('http://192.168.0.1:8080'); 77 | ``` 78 | 79 | ## API methods 80 | 81 | All URIs are relative to *http://localhost:8080/v1* 82 | 83 | Method | Description | HTTP request 84 | ------------ | ------------- | ------------- 85 | *getStatus* | Get status | [**GET** /](https://dkron.io/usage/api/#get) 86 | *getJobs* | Get all jobs | [**GET** /jobs](https://dkron.io/usage/api/#get-jobs) 87 | *saveJob* | Save job | [**POST** /jobs](https://dkron.io/usage/api/#post-jobs) 88 | *getJob* | Get job info by name | [**GET** /jobs/{job_name}](https://dkron.io/usage/api/#get-jobs-job-name) 89 | *runJob* | Run job by name | [**POST** /jobs/{job_name}](https://dkron.io/usage/api/#post-jobs-job-name) 90 | *deleteJob* | Delete job by name | [**DELETE** /jobs/{job_name}](https://dkron.io/usage/api/#delete-jobs-job-name) 91 | *getJobExecutions* | Get job executions by job name | [**GET** /jobs/{job_name}/executions](https://dkron.io/usage/api/#get-jobs-job-name-executions) 92 | *getLeader* | Get leader | [**GET** /leader](https://dkron.io/usage/api/#get-leader) 93 | *leave* | Force the node to leave the cluster | [**GET** /leave](https://dkron.io/usage/api/#get-leave) 94 | *getMembers* | Get members | [**GET** /members](https://dkron.io/usage/api/#get-members) 95 | 96 | 97 | ## Contribute 98 | 99 | Please refer to [CONTRIBUTING.md](https://github.com/gromo/dkron-php-adapter/blob/master/CONTRIBUTING.md) for information. 100 | -------------------------------------------------------------------------------- /tests/Models/JobTest.php: -------------------------------------------------------------------------------- 1 | 'test:name', 16 | 'schedule' => 'test:schedule', 17 | ]; 18 | $job = Job::createFromArray($mockData); 19 | 20 | $this->assertInstanceOf(Job::class, $job); 21 | $this->assertEquals($mockData['name'], $job->getName()); 22 | $this->assertEquals($mockData['schedule'], $job->getSchedule()); 23 | } 24 | 25 | public function testGetDataToSubmit() 26 | { 27 | $mockData = [ 28 | 'name' => 'test:name', 29 | 'schedule' => 'test:schedule', 30 | ]; 31 | 32 | $job = new Job( 33 | $mockData['name'], 34 | $mockData['schedule'], 35 | 777, 36 | 'Last Error Date', 37 | 'Last Success Date', 38 | 999 39 | ); 40 | $job->setExecutorConfig([]); 41 | $job->setProcessors([]); 42 | $job->setTags([]); 43 | 44 | $dataToSubmit = $job->getDataToSubmit(); 45 | 46 | $requiredFields = [ 47 | 'concurrency', 48 | 'dependent_jobs', 49 | 'disabled', 50 | 'executor', 51 | 'executor_config', 52 | 'name', 53 | 'owner', 54 | 'owner_email', 55 | 'parent_job', 56 | 'processors', 57 | 'retries', 58 | 'schedule', 59 | 'tags', 60 | 'timezone', 61 | ]; 62 | foreach ($requiredFields as $field) { 63 | $this->assertArrayHasKey($field, $dataToSubmit); 64 | } 65 | 66 | $readonlyFields = [ 67 | 'error_count', 68 | 'last_error', 69 | 'last_success', 70 | 'success_count', 71 | ]; 72 | foreach ($readonlyFields as $field) { 73 | $this->assertArrayNotHasKey($field, $dataToSubmit); 74 | } 75 | 76 | // check values 77 | foreach ($mockData as $key => $value) { 78 | $this->assertEquals($value, $dataToSubmit[$key]); 79 | } 80 | 81 | // check key-value objects 82 | $this->assertInstanceOf(stdClass::class, $dataToSubmit['executor_config']); 83 | $this->assertInstanceOf(stdClass::class, $dataToSubmit['processors']); 84 | $this->assertInstanceOf(stdClass::class, $dataToSubmit['tags']); 85 | } 86 | 87 | /** 88 | * @param string $value 89 | * @param string $exception 90 | * 91 | * @dataProvider setConcurrencyDataProvider 92 | */ 93 | public function testSetConcurrency($value, $exception = null) 94 | { 95 | $job = new Job('name', 'schedule'); 96 | 97 | if ($exception) { 98 | $this->expectException($exception); 99 | } 100 | $job->setConcurrency($value); 101 | $this->assertEquals($value, $job->getConcurrency()); 102 | } 103 | 104 | public function setConcurrencyDataProvider() 105 | { 106 | return [ 107 | 'success:allow' => [ 108 | 'value' => Job::CONCURRENCY_ALLOW, 109 | 'exception' => null, 110 | ], 111 | 'success:forbid' => [ 112 | 'value' => Job::CONCURRENCY_FORBID, 113 | 'exception' => null, 114 | ], 115 | 'error:empty' => [ 116 | 'value' => '', 117 | 'exception' => \InvalidArgumentException::class, 118 | ], 119 | 'error:invalid' => [ 120 | 'value' => 'invalid', 121 | 'exception' => \InvalidArgumentException::class, 122 | ], 123 | ]; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/EndpointsTest.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'endpoints' => 'http://192.168.0.1:8080/', 17 | ], 18 | 'success:endpointsAsArray' => [ 19 | 'endpoints' => [ 20 | 'http://192.168.0.1:8080/', 21 | 'https://example.com/', 22 | 'http://localhost', 23 | 'http://localhost/', 24 | 'http://localhost/test/1234?abc=123', 25 | ], 26 | ], 27 | 'error:endpointsAsNumber' => [ 28 | 'endpoints' => 10, 29 | 'exception' => InvalidArgumentException::class, 30 | ], 31 | 'error:endpointsAsInvalidUrl' => [ 32 | 'endpoints' => 'test.com', 33 | 'exception' => InvalidArgumentException::class, 34 | ], 35 | 'error:endpointsAsArray' => [ 36 | 'endpoints' => ['http://192.168.0.1:8080', 'http://localhost', 'test.com'], 37 | 'exception' => InvalidArgumentException::class, 38 | ], 39 | ]; 40 | } 41 | 42 | /** 43 | * @param mixed $endpoints 44 | * @param string|null $exception 45 | * 46 | * @dataProvider constructorDataProvider 47 | */ 48 | public function testConstructor($endpoints, string $exception = null) 49 | { 50 | if ($exception) { 51 | $this->expectException($exception); 52 | } 53 | $instance = new Endpoints($endpoints); 54 | 55 | if (!is_array($endpoints)) { 56 | $endpoints = [$endpoints]; 57 | } 58 | foreach ($endpoints as $endpoint) { 59 | $this->assertTrue($instance->hasEndpoint($endpoint)); 60 | } 61 | } 62 | 63 | public function testMethodGetAvailableEndpoint() 64 | { 65 | $endpoints = [ 66 | 'http://192.168.0.1/', 67 | 'http://192.168.0.2/', 68 | 'http://192.168.0.3/', 69 | ]; 70 | $instance = new Endpoints($endpoints); 71 | 72 | $availableEndpoints = []; 73 | for ($i = 0; $i < 9; $i++) { 74 | $availableEndpoint = $instance->getAvailableEndpoint(); 75 | if (!in_array($availableEndpoint, $availableEndpoints)) { 76 | $availableEndpoints[] = $availableEndpoint; 77 | } else { 78 | $this->assertEquals($availableEndpoints[$i % 3], $availableEndpoint); 79 | } 80 | } 81 | 82 | // handle exception if no endpoints available 83 | foreach ($endpoints as $endpoint) { 84 | $instance->setEndpointAsUnavailable($endpoint); 85 | } 86 | $this->expectException(DkronNoAvailableServersException::class); 87 | $instance->getAvailableEndpoint(); 88 | } 89 | 90 | public function testMethodGetAvailableEndpoints() 91 | { 92 | $endpoints = [ 93 | 'http://192.168.0.1:8080/', 94 | 'https://example.com/', 95 | 'http://localhost', 96 | 'http://localhost/', 97 | 'http://localhost/test/1234?abc=123', 98 | ]; 99 | $instance = new Endpoints($endpoints); 100 | 101 | $expectedEndpoints = [ 102 | 'http://192.168.0.1:8080', 103 | 'http://localhost', 104 | 'https://example.com', 105 | ]; 106 | for ($i = 0; $i < 3; $i++) { 107 | $availableEndpoints = $instance->getAvailableEndpoints(); 108 | sort($availableEndpoints); 109 | 110 | $this->assertEquals($expectedEndpoints, $availableEndpoints); 111 | $instance->setEndpointAsUnavailable(array_shift($expectedEndpoints)); 112 | } 113 | 114 | $availableEndpoints = $instance->getAvailableEndpoints(); 115 | $this->assertCount(0, $availableEndpoints); 116 | } 117 | 118 | public function testBooleanEndpointMethods() 119 | { 120 | $endpoints = [ 121 | 'http://192.168.0.1/', 122 | 'http://192.168.0.2/', 123 | ]; 124 | $instance = new Endpoints($endpoints); 125 | 126 | foreach ($endpoints as $endpoint) { 127 | $this->assertTrue($instance->hasEndpoint($endpoint)); 128 | $this->assertTrue($instance->isEndpointAvailable($endpoint)); 129 | $instance->setEndpointAsUnavailable($endpoint); 130 | $this->assertFalse($instance->isEndpointAvailable($endpoint)); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Endpoints.php: -------------------------------------------------------------------------------- 1 | sanitize($endpoint); 35 | }, $endpoints); 36 | $endpoints = array_unique($endpoints); 37 | 38 | shuffle($endpoints); 39 | 40 | foreach ($endpoints as $endpoint) { 41 | $this->endpoints[] = [ 42 | 'available' => true, 43 | 'url' => $endpoint, 44 | ]; 45 | } 46 | } 47 | 48 | /** 49 | * @return string 50 | * @throws DkronNoAvailableServersException 51 | */ 52 | public function getAvailableEndpoint(): string 53 | { 54 | $availableEndpoints = $this->getAvailableEndpoints(); 55 | $length = count($availableEndpoints); 56 | 57 | if ($length === 0) { 58 | throw new DkronNoAvailableServersException(); 59 | } 60 | if ($this->offset >= $length) { 61 | $this->offset = 0; 62 | } 63 | $endpoint = $availableEndpoints[$this->offset]; 64 | $this->offset = $this->offset + 1; 65 | 66 | return $endpoint; 67 | } 68 | 69 | /** 70 | * @return array[string] 71 | */ 72 | public function getAvailableEndpoints(): array 73 | { 74 | $availableEndpoints = array_values(array_filter($this->endpoints, function ($endpoint) { 75 | return $endpoint['available']; 76 | })); 77 | $availableEndpointsAsStrings = array_map(function ($endpoint) { 78 | return $endpoint['url']; 79 | }, $availableEndpoints); 80 | 81 | return $availableEndpointsAsStrings; 82 | } 83 | 84 | /** 85 | * @return int 86 | */ 87 | public function getSize(): int 88 | { 89 | return count($this->endpoints); 90 | } 91 | 92 | /** 93 | * @param string $endpoint 94 | * @return bool 95 | */ 96 | public function hasEndpoint(string $endpoint): bool 97 | { 98 | $url = $this->sanitize($endpoint); 99 | foreach ($this->endpoints as $i => $endpoint) { 100 | if ($endpoint['url'] === $url) { 101 | return true; 102 | } 103 | } 104 | return false; 105 | } 106 | 107 | /** 108 | * @param string $endpoint 109 | * @return bool 110 | */ 111 | public function isEndpointAvailable(string $endpoint): bool 112 | { 113 | $url = $this->sanitize($endpoint); 114 | foreach ($this->endpoints as $i => $endpoint) { 115 | if ($endpoint['url'] === $url) { 116 | return $endpoint['available']; 117 | } 118 | } 119 | return false; 120 | } 121 | 122 | /** 123 | * @param string $endpoint 124 | * @throws InvalidArgumentException 125 | */ 126 | public function setEndpointAsUnavailable(string $endpoint) 127 | { 128 | $url = $this->sanitize($endpoint); 129 | foreach ($this->endpoints as $i => $endpoint) { 130 | if ($endpoint['url'] === $url) { 131 | $this->endpoints[$i]['available'] = false; 132 | return; 133 | } 134 | } 135 | throw new InvalidArgumentException('Endpoint ' . $endpoint . ' not found'); 136 | } 137 | 138 | /** 139 | * @param string $endpoint 140 | * @return string 141 | * @throws InvalidArgumentException 142 | */ 143 | protected function sanitize(string $endpoint): string 144 | { 145 | if (filter_var($endpoint, FILTER_VALIDATE_URL) === false) { 146 | throw new InvalidArgumentException('Endpoint ' . $endpoint . ' has to be a valid URL'); 147 | } 148 | $url = parse_url($endpoint); 149 | $endpoint = $url['scheme'] . '://' . $url['host']; 150 | if (isset($url['port'])) { 151 | $endpoint .= ':' . $url['port']; 152 | } 153 | 154 | return strtolower($endpoint); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Models/Member.php: -------------------------------------------------------------------------------- 1 | name = $name; 69 | $this->addr = $addr; 70 | $this->port = $port; 71 | $this->tags = $tags; 72 | $this->status = $status; 73 | $this->protocolMin = $protocolMin; 74 | $this->protocolMax = $protocolMax; 75 | $this->protocolCur = $protocolCur; 76 | $this->delegateMin = $delegateMin; 77 | $this->delegateMax = $delegateMax; 78 | $this->delegateCur = $delegateCur; 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getName(): string 85 | { 86 | return $this->name; 87 | } 88 | 89 | /** 90 | * @return string 91 | */ 92 | public function getAddr(): string 93 | { 94 | return $this->addr; 95 | } 96 | 97 | /** 98 | * @return int 99 | */ 100 | public function getPort(): int 101 | { 102 | return $this->port; 103 | } 104 | 105 | /** 106 | * @return array 107 | */ 108 | public function getTags(): array 109 | { 110 | return $this->tags; 111 | } 112 | 113 | /** 114 | * @return int 115 | */ 116 | public function getStatus(): int 117 | { 118 | return $this->status; 119 | } 120 | 121 | /** 122 | * @return int 123 | */ 124 | public function getProtocolMin(): int 125 | { 126 | return $this->protocolMin; 127 | } 128 | 129 | /** 130 | * @return int 131 | */ 132 | public function getProtocolMax(): int 133 | { 134 | return $this->protocolMax; 135 | } 136 | 137 | /** 138 | * @return int 139 | */ 140 | public function getProtocolCur(): int 141 | { 142 | return $this->protocolCur; 143 | } 144 | 145 | /** 146 | * @return int 147 | */ 148 | public function getDelegateMin(): int 149 | { 150 | return $this->delegateMin; 151 | } 152 | 153 | /** 154 | * @return int 155 | */ 156 | public function getDelegateMax(): int 157 | { 158 | return $this->delegateMax; 159 | } 160 | 161 | /** 162 | * @return int 163 | */ 164 | public function getDelegateCur(): int 165 | { 166 | return $this->delegateCur; 167 | } 168 | 169 | 170 | public function jsonSerialize() 171 | { 172 | return [ 173 | 'Name' => $this->name, 174 | 'Addr' => $this->addr, 175 | 'Port' => $this->port, 176 | 'Tags' => $this->tags, 177 | 'Status' => $this->status, 178 | 'ProtocolMin' => $this->protocolMin, 179 | 'ProtocolMax' => $this->protocolMax, 180 | 'ProtocolCur' => $this->protocolCur, 181 | 'DelegateMin' => $this->delegateMin, 182 | 'DelegateMax' => $this->delegateMax, 183 | 'DelegateCur' => $this->delegateCur, 184 | ]; 185 | } 186 | 187 | public static function createFromArray(array $data) 188 | { 189 | return new static( 190 | $data['Name'] ?? null, 191 | $data['Addr'] ?? null, 192 | $data['Port'] ?? null, 193 | $data['Tags'] ?? null, 194 | $data['Status'] ?? null, 195 | $data['ProtocolMin'] ?? null, 196 | $data['ProtocolMax'] ?? null, 197 | $data['ProtocolCur'] ?? null, 198 | $data['DelegateMin'] ?? null, 199 | $data['DelegateMax'] ?? null, 200 | $data['DelegateCur'] ?? null 201 | ); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Api.php: -------------------------------------------------------------------------------- 1 | endpoints = $endpoints; 46 | 47 | if (is_null($httpClient)) { 48 | $httpClient = new Client([ 49 | 'timeout' => self::TIMEOUT, 50 | ]); 51 | } 52 | $this->httpClient = $httpClient; 53 | } 54 | 55 | /** 56 | * @param string $name 57 | * @throws DkronException 58 | */ 59 | public function deleteJob($name) 60 | { 61 | $this->request('/jobs/' . $name, self::METHOD_DELETE); 62 | } 63 | 64 | /** 65 | * @param string $name 66 | * @return Job 67 | * @throws DkronException 68 | */ 69 | public function getJob(string $name): Job 70 | { 71 | return Job::createFromArray($this->request('/jobs/' . $name)); 72 | } 73 | 74 | /** 75 | * @param $name 76 | * @return Execution[] 77 | * @throws DkronException 78 | */ 79 | public function getJobExecutions($name): array 80 | { 81 | $executions = []; 82 | $responseData = $this->request('/jobs/' . $name . '/executions'); 83 | foreach ($responseData as $executionData) { 84 | $executions[] = Execution::createFromArray($executionData); 85 | } 86 | 87 | return $executions; 88 | } 89 | 90 | /** 91 | * @return Job[] 92 | * @throws DkronException 93 | */ 94 | public function getJobs(): array 95 | { 96 | $jobs = []; 97 | $responseData = $this->request('/jobs'); 98 | foreach ($responseData as $jobData) { 99 | $jobs[] = Job::createFromArray($jobData); 100 | } 101 | 102 | return $jobs; 103 | } 104 | 105 | /** 106 | * @return Member 107 | * @throws DkronException 108 | */ 109 | public function getLeader(): Member 110 | { 111 | return Member::createFromArray($this->request('/leader')); 112 | } 113 | 114 | /** 115 | * @return Member[] 116 | * @throws DkronException 117 | */ 118 | public function getMembers(): array 119 | { 120 | $members = []; 121 | $responseData = $this->request('/members'); 122 | foreach ($responseData as $memberData) { 123 | $members[] = Member::createFromArray($memberData); 124 | } 125 | 126 | return $members; 127 | } 128 | 129 | /** 130 | * @return Status 131 | * @throws DkronException 132 | */ 133 | public function getStatus(): Status 134 | { 135 | return Status::createFromArray($this->request('/')); 136 | } 137 | 138 | /** 139 | * @param string $endpoint 140 | * @return Member[] 141 | * @throws DkronException 142 | */ 143 | public function leave(string $endpoint = null): array 144 | { 145 | if (is_null($endpoint) && $this->endpoints->getSize() === 1) { 146 | $endpoint = $this->endpoints->getAvailableEndpoint(); 147 | } 148 | if (is_null($endpoint)) { 149 | throw new InvalidArgumentException('Parameter endpoint has to be set'); 150 | } 151 | 152 | $members = []; 153 | $responseData = $this->request('/leave', self::METHOD_GET, null, $endpoint); 154 | foreach ($responseData as $memberData) { 155 | $members[] = Member::createFromArray($memberData); 156 | } 157 | 158 | return $members; 159 | } 160 | 161 | /** 162 | * @param string $name 163 | * @throws DkronException 164 | */ 165 | public function runJob($name) 166 | { 167 | $this->request('/jobs/' . $name, self::METHOD_POST); 168 | } 169 | 170 | /** 171 | * @param Job $job 172 | * @throws DkronException 173 | */ 174 | public function saveJob(Job $job) 175 | { 176 | $this->request('/jobs', self::METHOD_POST, $job->getDataToSubmit()); 177 | } 178 | 179 | /** 180 | * @param string $url 181 | * @param string $method 182 | * @param mixed $data 183 | * @param array|string|Endpoints $endpoints 184 | * @return array|null 185 | * @throws DkronException 186 | */ 187 | protected function request($url, $method = self::METHOD_GET, $data = null, $endpoints = null) 188 | { 189 | if (is_null($endpoints)) { 190 | $endpoints = $this->endpoints; 191 | } 192 | if (!($endpoints instanceof Endpoints)) { 193 | $endpoints = new Endpoints($endpoints); 194 | } 195 | 196 | while ($endpoint = $endpoints->getAvailableEndpoint()) { 197 | try { 198 | /** @var Response $response */ 199 | $response = $this->httpClient->request($method, $endpoint . self::URL_PREFIX . ltrim($url, '/'), [ 200 | 'json' => $data, 201 | ]); 202 | 203 | $data = json_decode($response->getBody(), true); 204 | if (JSON_ERROR_NONE !== json_last_error()) { 205 | throw new DkronResponseException('json_decode error: ' . json_last_error_msg()); 206 | } 207 | 208 | return $data; 209 | } catch (ConnectException $exception) { 210 | $this->endpoints->setEndpointAsUnavailable($endpoint); 211 | } catch (DkronException $exception) { 212 | throw $exception; 213 | } catch (\Throwable $exception) { 214 | throw new DkronException($exception->getMessage()); 215 | } 216 | } 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /src/Models/Job.php: -------------------------------------------------------------------------------- 1 | name = $name; 85 | $this->setSchedule($schedule); 86 | 87 | // read-only parameters 88 | $this->errorCount = is_int($errorCount) ? $errorCount : 0; 89 | $this->lastError = is_string($lastError) ? $lastError : ''; 90 | $this->lastSuccess = is_string($lastSuccess) ? $lastSuccess : ''; 91 | $this->successCount = is_int($successCount) ? $successCount : 0; 92 | } 93 | 94 | public function disableConcurrency(): self 95 | { 96 | return $this->setConcurrency(self::CONCURRENCY_FORBID); 97 | } 98 | 99 | public function enableConcurrency(): self 100 | { 101 | return $this->setConcurrency(self::CONCURRENCY_ALLOW); 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | public function getConcurrency(): string 108 | { 109 | return $this->concurrency; 110 | } 111 | 112 | /** 113 | * Get data to be serialized as json for API 114 | * All fields must be sent in order to overwrite values 115 | * 116 | * @return array[string]string 117 | */ 118 | public function getDataToSubmit(): array 119 | { 120 | return [ 121 | 'name' => $this->name, 122 | 'schedule' => $this->schedule, 123 | 'concurrency' => $this->concurrency, 124 | 'dependent_jobs' => $this->dependentJobs, 125 | 'disabled' => $this->disabled, 126 | 'executor' => $this->executor, 127 | 'executor_config' => (object)$this->executorConfig, 128 | 'owner' => $this->owner, 129 | 'owner_email' => $this->ownerEmail, 130 | 'parent_job' => $this->parentJob, 131 | 'processors' => (object)$this->processors, 132 | 'retries' => $this->retries, 133 | 'tags' => (object)$this->tags, 134 | 'timezone' => $this->timezone, 135 | ]; 136 | } 137 | 138 | /** 139 | * @return string[] 140 | */ 141 | public function getDependentJobs(): array 142 | { 143 | return $this->dependentJobs; 144 | } 145 | 146 | /** 147 | * @return bool 148 | */ 149 | public function getDisabled(): bool 150 | { 151 | return $this->disabled; 152 | } 153 | 154 | /** 155 | * @return int 156 | */ 157 | public function getErrorCount(): int 158 | { 159 | return $this->errorCount; 160 | } 161 | 162 | /** 163 | * @return string 164 | */ 165 | public function getExecutor(): string 166 | { 167 | return $this->executor; 168 | } 169 | 170 | /** 171 | * @return array[string]string 172 | */ 173 | public function getExecutorConfig(): array 174 | { 175 | return $this->executorConfig; 176 | } 177 | 178 | /** 179 | * @return string 180 | */ 181 | public function getLastError(): string 182 | { 183 | return $this->lastError; 184 | } 185 | 186 | /** 187 | * @return string 188 | */ 189 | public function getLastSuccess(): string 190 | { 191 | return $this->lastSuccess; 192 | } 193 | 194 | /** 195 | * @return string 196 | */ 197 | public function getName(): string 198 | { 199 | return $this->name; 200 | } 201 | 202 | /** 203 | * @return string 204 | */ 205 | public function getOwner(): string 206 | { 207 | return $this->owner; 208 | } 209 | 210 | /** 211 | * @return string 212 | */ 213 | public function getOwnerEmail(): string 214 | { 215 | return $this->ownerEmail; 216 | } 217 | 218 | /** 219 | * @return string 220 | */ 221 | public function getParentJob(): string 222 | { 223 | return $this->parentJob; 224 | } 225 | 226 | /** 227 | * @return array[string]string 228 | */ 229 | public function getProcessors(): array 230 | { 231 | return $this->processors; 232 | } 233 | 234 | /** 235 | * @return int 236 | */ 237 | public function getRetries(): int 238 | { 239 | return $this->retries; 240 | } 241 | 242 | /** 243 | * @return string 244 | */ 245 | public function getSchedule(): string 246 | { 247 | return $this->schedule; 248 | } 249 | 250 | /** 251 | * @return int 252 | */ 253 | public function getSuccessCount(): int 254 | { 255 | return $this->successCount; 256 | } 257 | 258 | /** 259 | * @return array[string]string 260 | */ 261 | public function getTags(): array 262 | { 263 | return $this->tags; 264 | } 265 | 266 | /** 267 | * @return string 268 | */ 269 | public function getTimezone(): string 270 | { 271 | return $this->timezone; 272 | } 273 | 274 | /** 275 | * @return bool 276 | */ 277 | public function isDisabled(): bool 278 | { 279 | return $this->disabled; 280 | } 281 | 282 | /** 283 | * @return array[string]string 284 | */ 285 | public function jsonSerialize(): array 286 | { 287 | return [ 288 | 'name' => $this->name, 289 | 'schedule' => $this->schedule, 290 | 'concurrency' => $this->concurrency, 291 | 'dependent_jobs' => $this->dependentJobs, 292 | 'disabled' => $this->disabled, 293 | 'error_count' => $this->errorCount, 294 | 'executor' => $this->executor, 295 | 'executor_config' => (object)$this->executorConfig, 296 | 'last_error' => $this->lastError, 297 | 'last_success' => $this->lastSuccess, 298 | 'owner' => $this->owner, 299 | 'owner_email' => $this->ownerEmail, 300 | 'parent_job' => $this->parentJob, 301 | 'processors' => (object)$this->processors, 302 | 'retries' => $this->retries, 303 | 'success_count' => $this->successCount, 304 | 'tags' => (object)$this->tags, 305 | 'timezone' => $this->timezone, 306 | ]; 307 | } 308 | 309 | /** 310 | * @param string $concurrency 311 | * @return $this 312 | * @throws InvalidArgumentException 313 | */ 314 | public function setConcurrency(string $concurrency): self 315 | { 316 | if (!in_array($concurrency, [self::CONCURRENCY_ALLOW, self::CONCURRENCY_FORBID], true)) { 317 | throw new InvalidArgumentException('Concurrency value is incorrect. Allowed values are ' 318 | . self::CONCURRENCY_ALLOW . ' or ' . self::CONCURRENCY_FORBID); 319 | } 320 | $this->concurrency = $concurrency; 321 | 322 | return $this; 323 | } 324 | 325 | /** 326 | * @param string[] $dependentJobs 327 | * @return $this 328 | */ 329 | public function setDependentJobs(array $dependentJobs): self 330 | { 331 | $this->dependentJobs = $dependentJobs; 332 | 333 | return $this; 334 | } 335 | 336 | /** 337 | * @param bool $disabled 338 | * @return $this 339 | */ 340 | public function setDisabled(bool $disabled): self 341 | { 342 | $this->disabled = (bool)$disabled; 343 | 344 | return $this; 345 | } 346 | 347 | /** 348 | * @param string $executor 349 | * @return $this 350 | */ 351 | public function setExecutor(string $executor) 352 | { 353 | $this->executor = $executor; 354 | 355 | return $this; 356 | } 357 | 358 | /** 359 | * @param array[string]string $executorConfig 360 | * @return $this 361 | */ 362 | public function setExecutorConfig(array $executorConfig) 363 | { 364 | $this->executorConfig = $executorConfig; 365 | 366 | return $this; 367 | } 368 | 369 | /** 370 | * @param string $owner 371 | * @return $this 372 | */ 373 | public function setOwner(string $owner): self 374 | { 375 | $this->owner = $owner; 376 | 377 | return $this; 378 | } 379 | 380 | /** 381 | * @param string $ownerEmail 382 | * @return $this 383 | */ 384 | public function setOwnerEmail(string $ownerEmail): self 385 | { 386 | $this->ownerEmail = $ownerEmail; 387 | 388 | return $this; 389 | } 390 | 391 | /** 392 | * @param string $parentJob 393 | * @return $this 394 | */ 395 | public function setParentJob(string $parentJob): self 396 | { 397 | $this->parentJob = $parentJob; 398 | 399 | return $this; 400 | } 401 | 402 | /** 403 | * @param array[string]string $processors 404 | * @return $this 405 | */ 406 | public function setProcessors(array $processors): self 407 | { 408 | $this->processors = $processors; 409 | 410 | return $this; 411 | } 412 | 413 | /** 414 | * @param int $retries 415 | * @return $this 416 | */ 417 | public function setRetries(int $retries): self 418 | { 419 | $this->retries = $retries; 420 | 421 | return $this; 422 | } 423 | 424 | /** 425 | * @param string $schedule 426 | * @return $this 427 | */ 428 | public function setSchedule($schedule): self 429 | { 430 | $this->schedule = $schedule; 431 | 432 | return $this; 433 | } 434 | 435 | /** 436 | * @param array[string]string $tags 437 | * @return $this 438 | */ 439 | public function setTags(array $tags): self 440 | { 441 | $this->tags = $tags; 442 | 443 | return $this; 444 | } 445 | 446 | /** 447 | * @param string $timezone 448 | * @return $this 449 | */ 450 | public function setTimezone(string $timezone) 451 | { 452 | $this->timezone = $timezone; 453 | 454 | return $this; 455 | } 456 | 457 | /** 458 | * @param array[string]string $data 459 | * @return Job 460 | */ 461 | public static function createFromArray(array $data): self 462 | { 463 | // create job with required and read-only data 464 | $job = new self( 465 | $data['name'], 466 | $data['schedule'], 467 | $data['error_count'] ?? null, 468 | $data['last_error'] ?? null, 469 | $data['last_success'] ?? null, 470 | $data['success_count'] ?? null 471 | ); 472 | if (isset($data['concurrency'])) { 473 | $job->setConcurrency($data['concurrency']); 474 | } 475 | if (isset($data['disabled'])) { 476 | $job->setDisabled($data['disabled']); 477 | } 478 | if (isset($data['executor'])) { 479 | $job->setExecutor($data['executor']); 480 | } 481 | if (isset($data['executor_config'])) { 482 | $job->setExecutorConfig($data['executor_config']); 483 | } 484 | if (isset($data['owner'])) { 485 | $job->setOwner($data['owner']); 486 | } 487 | if (isset($data['owner_email'])) { 488 | $job->setOwnerEmail($data['owner_email']); 489 | } 490 | if (isset($data['parent_job'])) { 491 | $job->setParentJob($data['parent_job']); 492 | } 493 | if (isset($data['retries'])) { 494 | $job->setRetries($data['retries']); 495 | } 496 | if (isset($data['timezone'])) { 497 | $job->setTimezone($data['timezone']); 498 | } 499 | 500 | // nullable values 501 | if (!empty($data['dependent_jobs'])) { 502 | $job->setDependentJobs($data['dependent_jobs']); 503 | } 504 | if (!empty($data['processors'])) { 505 | $job->setProcessors($data['processors']); 506 | } 507 | if (!empty($data['tags'])) { 508 | $job->setTags($data['tags']); 509 | } 510 | 511 | return $job; 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /tests/ApiTest.php: -------------------------------------------------------------------------------- 1 | getHttpClient(); 31 | $defaults->client = null; 32 | 33 | return [ 34 | 'success:defaults' => [ 35 | 'http' => $defaults, 36 | ], 37 | 'success:endpointsAsString' => [ 38 | 'http' => $this->getHttpClient(null, 'http://192.168.0.1:8080/') 39 | ], 40 | 'success:endpointsAsArray' => [ 41 | 'http' => $this->getHttpClient(null, [ 42 | 'http://192.168.0.1:8080/', 43 | 'http://localhost/', 44 | 'https://example.com/' 45 | ]), 46 | ], 47 | 'error:endpointsAsNumber' => [ 48 | 'http' => $this->getHttpClient(null, 10), 49 | 'exception' => InvalidArgumentException::class, 50 | ], 51 | 'error:endpointsAsInvalidUrl' => [ 52 | 'http' => $this->getHttpClient(null, 'test.com'), 53 | 'exception' => InvalidArgumentException::class, 54 | ], 55 | ]; 56 | } 57 | 58 | /** 59 | * @param mixed $http 60 | * @param string|null $exception 61 | * 62 | * @dataProvider constructorDataProvider 63 | */ 64 | public function testConstructor($http, string $exception = null) 65 | { 66 | if ($exception) { 67 | $this->expectException($exception); 68 | } 69 | $api = new Api($http->endpoints, $http->client); 70 | 71 | // check api was created successfully 72 | $this->assertInstanceOf(Api::class, $api); 73 | } 74 | 75 | /** 76 | * Make sure all servers from the list were called 77 | */ 78 | public function testAllEndpointsCalled() 79 | { 80 | $request = new Request('GET', ''); 81 | $http = $this->getHttpClient([ 82 | new ConnectException('Client Error', $request), 83 | new ConnectException('Client Error', $request), 84 | new ConnectException('Client Error', $request), 85 | ], [ 86 | 'http://192.168.0.1/', 87 | 'http://192.168.0.2/', 88 | 'http://192.168.0.3/' 89 | ]); 90 | 91 | $api = new Api($http->endpoints, $http->client); 92 | $exceptionHandled = false; 93 | 94 | try { 95 | $api->getStatus(); 96 | } catch (Exception $exception) { 97 | $this->assertInstanceOf(DkronNoAvailableServersException::class, $exception); 98 | $exceptionHandled = true; 99 | } 100 | 101 | $this->assertTrue($exceptionHandled); 102 | $this->assertCount(3, $http->transactions); 103 | 104 | } 105 | 106 | public function testMethodDeleteJob() 107 | { 108 | $http = $this->getHttpClient(); 109 | $api = new Api($http->endpoints, $http->client); 110 | $jobName = 'job001'; 111 | 112 | $api->deleteJob($jobName); 113 | 114 | $request = $this->getRequest($http); 115 | $this->assertEquals('/v1/jobs/' . $jobName, $request->getUri()->getPath()); 116 | $this->assertEquals('DELETE', mb_strtoupper($request->getMethod())); 117 | } 118 | 119 | public function testMethodGetJob() 120 | { 121 | $mockData = [ 122 | 'name' => 'test:name', 123 | 'schedule' => 'test:schedule', 124 | ]; 125 | $http = $this->getHttpClient([$mockData]); 126 | $api = new Api($http->endpoints, $http->client); 127 | 128 | $job = $api->getJob($mockData['name']); 129 | 130 | // check request 131 | $request = $this->getRequest($http); 132 | $this->assertEquals('/v1/jobs/' . $mockData['name'], $request->getUri()->getPath()); 133 | $this->assertEquals('GET', mb_strtoupper($request->getMethod())); 134 | 135 | // check result 136 | $this->assertInstanceOf(Job::class, $job); 137 | $this->assertEquals($mockData['name'], $job->getName()); 138 | $this->assertEquals($mockData['schedule'], $job->getSchedule()); 139 | } 140 | 141 | public function testMethodGetJobExecutions() 142 | { 143 | $mockData = [ 144 | ['job_name' => 'nameA', 'success' => true], 145 | ['job_name' => 'nameB', 'success' => false], 146 | ]; 147 | $http = $this->getHttpClient([$mockData]); 148 | $api = new Api($http->endpoints, $http->client); 149 | $jobName = 'job001'; 150 | 151 | $executions = $api->getJobExecutions($jobName); 152 | 153 | // check request 154 | $request = $this->getRequest($http); 155 | $this->assertEquals('/v1/jobs/' . $jobName . '/executions', $request->getUri()->getPath()); 156 | $this->assertEquals('GET', mb_strtoupper($request->getMethod())); 157 | 158 | // check result 159 | $this->assertCount(2, $executions); 160 | foreach ($mockData as $i => $executionData) { 161 | $execution = $executions[$i]; 162 | 163 | $this->assertInstanceOf(Execution::class, $execution); 164 | $this->assertEquals($executionData['job_name'], $execution->getJobName()); 165 | $this->assertEquals($executionData['success'], $execution->isSuccess()); 166 | } 167 | } 168 | 169 | public function testMethodGetJobs() 170 | { 171 | $mockData = [ 172 | ['name' => 'nameA', 'schedule' => 'scheduleA'], 173 | ['name' => 'nameB', 'schedule' => 'scheduleB'], 174 | ]; 175 | $http = $this->getHttpClient([$mockData]); 176 | $api = new Api($http->endpoints, $http->client); 177 | 178 | $jobs = $api->getJobs(); 179 | 180 | // check request 181 | $request = $this->getRequest($http); 182 | $this->assertEquals('/v1/jobs', $request->getUri()->getPath()); 183 | $this->assertEquals('GET', mb_strtoupper($request->getMethod())); 184 | 185 | // check result 186 | $this->assertCount(2, $jobs); 187 | foreach ($mockData as $i => $jobData) { 188 | $job = $jobs[$i]; 189 | $this->assertInstanceOf(Job::class, $job); 190 | $this->assertEquals($jobData['name'], $job->getName()); 191 | $this->assertEquals($jobData['schedule'], $job->getSchedule()); 192 | } 193 | } 194 | 195 | public function testMethodGetLeader() 196 | { 197 | $mockData = [ 198 | 'Name' => 'leader:name', 199 | 'Addr' => 'leader:addr', 200 | ]; 201 | $http = $this->getHttpClient([$mockData]); 202 | $api = new Api($http->endpoints, $http->client); 203 | 204 | $leader = $api->getLeader(); 205 | 206 | // check request 207 | $request = $this->getRequest($http); 208 | $this->assertEquals('/v1/leader', $request->getUri()->getPath()); 209 | $this->assertEquals('GET', mb_strtoupper($request->getMethod())); 210 | 211 | // check result 212 | $this->assertInstanceOf(Member::class, $leader); 213 | $this->assertEquals($mockData['Name'], $leader->getName()); 214 | $this->assertEquals($mockData['Addr'], $leader->getAddr()); 215 | } 216 | 217 | public function testMethodGetMembers() 218 | { 219 | $mockData = [ 220 | ['Name' => 'nameA', 'Addr' => 'addrA'], 221 | ['Name' => 'nameB', 'Addr' => 'addrB'], 222 | ]; 223 | $http = $this->getHttpClient([$mockData]); 224 | $api = new Api($http->endpoints, $http->client); 225 | 226 | $members = $api->getMembers(); 227 | 228 | // check request 229 | $request = $this->getRequest($http); 230 | $this->assertEquals('/v1/members', $request->getUri()->getPath()); 231 | $this->assertEquals('GET', mb_strtoupper($request->getMethod())); 232 | 233 | // check result 234 | $this->assertCount(2, $members); 235 | foreach ($mockData as $i => $mockItemData) { 236 | $member = $members[$i]; 237 | 238 | $this->assertInstanceOf(Member::class, $member); 239 | $this->assertEquals($mockItemData['Name'], $member->getName()); 240 | $this->assertEquals($mockItemData['Addr'], $member->getAddr()); 241 | } 242 | } 243 | 244 | public function testMethodGetStatus() 245 | { 246 | $mockData = [ 247 | 'agent' => [ 248 | 'backend' => 'consul', 249 | 'name' => '217f633ff07d', 250 | 'version' => '0.10.0', 251 | ], 252 | 'serf' => [ 253 | 'encrypted' => 'false', 254 | 'event_queue' => '0', 255 | 'event_time' => '1', 256 | 'failed' => '0', 257 | ], 258 | 'tags' => [ 259 | 'dkron_rpc_addr' => '172.21.0.7:6868', 260 | 'dkron_server' => 'true', 261 | 'dkron_version' => '0.10.0', 262 | ] 263 | ]; 264 | $http = $this->getHttpClient([$mockData]); 265 | $api = new Api($http->endpoints, $http->client); 266 | 267 | $status = $api->getStatus(); 268 | 269 | // check request 270 | $request = $this->getRequest($http); 271 | $this->assertEquals('/v1/', $request->getUri()->getPath()); 272 | $this->assertEquals('GET', mb_strtoupper($request->getMethod())); 273 | 274 | // check result 275 | $this->assertInstanceOf(Status::class, $status); 276 | $this->assertEquals($mockData['agent'], $status->getAgent()); 277 | $this->assertEquals($mockData['serf'], $status->getSerf()); 278 | $this->assertEquals($mockData['tags'], $status->getTags()); 279 | } 280 | 281 | public function testMethodLeaveWithOneEndpoint() 282 | { 283 | $mockData = [ 284 | ['Name' => 'nameA', 'Addr' => 'addrA'], 285 | ['Name' => 'nameB', 'Addr' => 'addrB'], 286 | ]; 287 | $http = $this->getHttpClient([$mockData]); 288 | $api = new Api($http->endpoints, $http->client); 289 | 290 | $members = $api->leave(); 291 | 292 | // check request 293 | $request = $this->getRequest($http); 294 | $this->assertEquals('/v1/leave', $request->getUri()->getPath()); 295 | $this->assertEquals('GET', mb_strtoupper($request->getMethod())); 296 | 297 | // check result 298 | $this->assertCount(2, $members); 299 | foreach ($mockData as $i => $mockItemData) { 300 | $member = $members[$i]; 301 | 302 | $this->assertInstanceOf(Member::class, $member); 303 | $this->assertEquals($mockItemData['Name'], $member->getName()); 304 | $this->assertEquals($mockItemData['Addr'], $member->getAddr()); 305 | } 306 | } 307 | 308 | public function testMethodLeaveWithEmptyEndpoint() 309 | { 310 | $mockData = [ 311 | ['Name' => 'nameA', 'Addr' => 'addrA'], 312 | ['Name' => 'nameB', 'Addr' => 'addrB'], 313 | ]; 314 | $mockEndpoints = [ 315 | 'http://192.168.0.1', 316 | 'http://192.168.0.2', 317 | 'http://192.168.0.3', 318 | ]; 319 | $http = $this->getHttpClient([$mockData], $mockEndpoints); 320 | $api = new Api($http->endpoints, $http->client); 321 | 322 | $this->expectException(InvalidArgumentException::class); 323 | $api->leave(); 324 | } 325 | 326 | public function testMethodLeaveWithSpecificEndpoint() 327 | { 328 | $mockData = [ 329 | ['Name' => 'nameA', 'Addr' => 'addrA'], 330 | ['Name' => 'nameB', 'Addr' => 'addrB'], 331 | ]; 332 | $mockEndpoints = [ 333 | 'http://192.168.0.1', 334 | 'http://192.168.0.2', 335 | 'http://192.168.0.3', 336 | ]; 337 | $http = $this->getHttpClient([$mockData], $mockEndpoints); 338 | $api = new Api($http->endpoints, $http->client); 339 | 340 | $members = $api->leave($mockEndpoints[0]); 341 | $this->assertCount(2, $members); 342 | } 343 | 344 | public function testMethodRunJob() 345 | { 346 | $http = $this->getHttpClient(); 347 | $api = new Api($http->endpoints, $http->client); 348 | $jobName = 'job001'; 349 | 350 | $api->runJob($jobName); 351 | 352 | // check request 353 | $request = $this->getRequest($http); 354 | $this->assertEquals('/v1/jobs/' . $jobName, $request->getUri()->getPath()); 355 | $this->assertEquals('POST', mb_strtoupper($request->getMethod())); 356 | } 357 | 358 | public function testMethodSaveJob() 359 | { 360 | $mockData = [ 361 | 'name' => 'test:name', 362 | 'schedule' => 'test:schedule', 363 | 'executor' => 'shell', 364 | 'executor_config' => [ 365 | 'command' => 'ls -la /tmp', 366 | 'shell' => true 367 | ], 368 | 'processors' => [ 369 | 'log' => [ 370 | 'forward' => true, 371 | ], 372 | ], 373 | ]; 374 | $http = $this->getHttpClient([$mockData]); 375 | $api = new Api($http->endpoints, $http->client); 376 | $job = Job::createFromArray($mockData); 377 | 378 | $api->saveJob($job); 379 | 380 | // check request 381 | $request = $this->getRequest($http); 382 | $this->assertEquals('/v1/jobs', $request->getUri()->getPath()); 383 | $this->assertEquals('POST', mb_strtoupper($request->getMethod())); 384 | $requestData = json_decode($request->getBody()->__toString(), true); 385 | $this->assertArraySubset($mockData, $requestData); 386 | } 387 | 388 | 389 | /** 390 | * @param array $responses 391 | * @param mixed $endpoints 392 | * @return stdClass 393 | */ 394 | protected function getHttpClient(array $responses = null, $endpoints = 'http://127.0.0.1/'): stdClass 395 | { 396 | $output = new stdClass(); 397 | $output->endpoints = $endpoints; 398 | $output->transactions = []; 399 | 400 | if (is_null($responses)) { 401 | $responses = [null]; 402 | } 403 | $responses = array_map(function ($response) { 404 | return ($response instanceof ResponseInterface) || ($response instanceof RequestException) 405 | ? $response 406 | : new Response(200, ['Content-Type: application/json'], json_encode($response)); 407 | }, $responses); 408 | 409 | $handler = HandlerStack::create(new MockHandler($responses)); 410 | $handler->push(Middleware::history($output->transactions)); 411 | 412 | $output->client = new Client([ 413 | 'handler' => $handler, 414 | ]); 415 | 416 | return $output; 417 | } 418 | 419 | protected function getRequest($http): RequestInterface 420 | { 421 | $this->assertCount(1, $http->transactions, 'Request is not available in transactions'); 422 | return $http->transactions[0]['request']; 423 | } 424 | } 425 | --------------------------------------------------------------------------------