diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..3752010 --- /dev/null +++ b/src/Client.php @@ -0,0 +1,780 @@ +host = $host; + $this->port = $port; + $this->version = trim($version); + } + + public function setHttpOptions(array $options) + { + $this->httpOptions = $options; + } + + public function setPretty($enabled) + { + $this->pretty = $enabled; + } + + public function setToken($token) + { + $this->token = $token; + } + + public function clearToken() + { + $this->token = null; + } + + // region kv + + /** + * Put puts the given key into the key-value store. + * A put request increments the revision of the key-value + * store\nand generates one event in the event history. + * + * @param string $key + * @param string $value + * @param array $options 可选参数 + * int64 lease + * bool prev_kv + * bool ignore_value + * bool ignore_lease + * @return array + * @throws + */ + public function put(string $key, string $value, array $options = []): array + { + $params = ['value' => $value]; + + $key = ltrim($key,'/'); + + $body = $this->request('POST', "/v2/keys/$key", $params, $options); + $body = $this->decodeBodyForFields( + $body, + 'prev_kv', + ['key', 'value',] + ); + + if (isset($body['prev_kv']) && $this->pretty) { + return $this->convertFields($body['prev_kv']); + } + + return $body; + } + + /** + * Gets the key or a range of keys + * + * @param string $key + * @param array $options + * string range_end + * int limit + * int revision + * int sort_order + * int sort_target + * bool serializable + * bool keys_only + * bool count_only + * int64 min_mod_revision + * int64 max_mod_revision + * int64 min_create_revision + * int64 max_create_revision + * @return array + * @throws Exception + */ + public function get(string $key): array + { + $key = ltrim($key,'/'); + + $body = $this->request('GET', "/v2/keys/$key"); + $body = $this->decodeBodyForFields( + $body, + 'kvs', + ['key', 'value',] + ); + + if (isset($body['kvs']) && $this->pretty) { + return $this->convertFields($body['kvs']); + } + + return $body; + } + + /** + * get all keys + * + * @return array + * @throws Exception + */ + public function getAllKeys(): array + { + return $this->get('/'); + } + + /** + * get all keys with prefix + * + * @param string $prefix + * @return array + * @throws Exception + */ + public function getKeysWithPrefix(string $prefix): array + { + $prefix = trim($prefix); + if (!$prefix) { + return []; + } + $lastIndex = strlen($prefix) - 1; + $lastChar = $prefix[$lastIndex]; + $nextAsciiCode = ord($lastChar) + 1; + $rangeEnd = $prefix; + $rangeEnd[$lastIndex] = chr($nextAsciiCode); + + return $this->get($prefix, ['range_end' => $rangeEnd]); + } + + /** + * Removes the specified key or range of keys + * + * @param string $key + * @return array + * @throws Exception + */ + public function del(string $key): array + { + $key = ltrim($key,'/'); + $body = $this->request('DELETE', "/v2/keys/$key"); + $body = $this->decodeBodyForFields( + $body, + 'prev_kvs', + ['key', 'value',] + ); + if (isset($body['prev_kvs']) && $this->pretty) { + return $this->convertFields($body['prev_kvs']); + } + return $body; + } + + /** + * Compact compacts the event history in the etcd key-value store. + * The key-value\nstore should be periodically compacted + * or the event history will continue to grow\nindefinitely. + * + * @param int $revision + * + * @param bool|false $physical + * + * @return array + * @throws Exception + */ + public function compaction($revision, $physical = false) + { + $params = [ + 'revision' => $revision, + 'physical' => $physical, + ]; + + return $this->request(self::URI_COMPACTION, $params); + } + + // endregion kv + + // region lease + + /** + * LeaseGrant creates a lease which expires if the server does not receive a + * keepAlive\nwithin a given time to live period. All keys attached to the lease + * will be expired and\ndeleted if the lease expires. + * Each expired key generates a delete event in the event history.", + * + * @param int $ttl TTL is the advisory time-to-live in seconds. + * @param int $id ID is the requested ID for the lease. + * If ID is set to 0, the lessor chooses an ID. + * @return array + */ + public function grant($ttl, $id = 0) + { + $params = [ + 'TTL' => $ttl, + 'ID' => $id, + ]; + + + return $this->request(self::URI_GRANT, $params); + } + + /** + * revokes a lease. All keys attached to the lease will expire and be deleted. + * + * @param int $id ID is the lease ID to revoke. When the ID is revoked, + * all associated keys will be deleted. + * @return array + */ + public function revoke($id) + { + $params = [ + 'ID' => $id, + ]; + + return $this->request(self::URI_REVOKE, $params); + } + + /** + * keeps the lease alive by streaming keep alive requests + * from the client\nto the server and streaming keep alive responses + * from the server to the client. + * + * @param int $id ID is the lease ID for the lease to keep alive. + * @return array + */ + public function keepAlive(int $id): array + { + $params = [ + 'ID' => $id, + ]; + + $body = $this->request(self::URI_KEEPALIVE, $params); + + if (!isset($body['result'])) { + return $body; + } + // response "result" field, etcd bug? + return [ + 'ID' => $body['result']['ID'], + 'TTL' => $body['result']['TTL'], + ]; + } + + /** + * retrieves lease information. + * + * @param int $id ID is the lease ID for the lease. + * @param bool|false $keys + * @return array + */ + public function timeToLive(int $id, bool $keys): array + { + $params = [ + 'ID' => $id, + 'keys' => $keys, + ]; + + $body = $this->request(self::URI_TIMETOLIVE, $params); + + if (isset($body['keys'])) { + $body['keys'] = array_map(function ($value) { + return base64_decode($value); + }, $body['keys']); + } + + return $body; + } + + // endregion lease + + // region auth + + /** + * enable authentication + * + * @return array + */ + public function authEnable() + { + $body = $this->request(self::URI_AUTH_ENABLE); + $this->clearToken(); + + return $body; + } + + /** + * disable authentication + * + * @return array + */ + public function authDisable() + { + $body = $this->request(self::URI_AUTH_DISABLE); + $this->clearToken(); + + return $body; + } + + /** + * @param string $user + * @param string $password + * @return array + */ + public function authenticate($user, $password) + { + $params = [ + 'name' => $user, + 'password' => $password, + ]; + + $body = $this->request(self::URI_AUTH_AUTHENTICATE, $params); + if ($this->pretty && isset($body['token'])) { + return $body['token']; + } + + return $body; + } + + /** + * add a new role. + * + * @param string $name + * @return array + */ + public function addRole($name) + { + $params = [ + 'name' => $name, + ]; + + $body = $this->request(self::URI_AUTH_ROLE_ADD, $params); + + return $body; + } + + /** + * get detailed role information. + * + * @param string $role + * @return array + */ + public function getRole($role) + { + $params = [ + 'role' => $role, + ]; + + $body = $this->request(self::URI_AUTH_ROLE_GET, $params); + $body = $this->decodeBodyForFields( + $body, + 'perm', + ['key', 'range_end',] + ); + if ($this->pretty && isset($body['perm'])) { + return $body['perm']; + } + + return $body; + } + + /** + * delete a specified role. + * + * @param string $role + * @return array + */ + public function deleteRole($role) + { + $params = [ + 'role' => $role, + ]; + + return $this->request(self::URI_AUTH_ROLE_DELETE, $params); + } + + /** + * get lists of all roles + * + * @return array + */ + public function roleList() + { + $body = $this->request(self::URI_AUTH_ROLE_LIST); + + if ($this->pretty && isset($body['roles'])) { + return $body['roles']; + } + + return $body; + } + + /** + * add a new user + * + * @param string $user + * @param string $password + * @return array + */ + public function addUser($user, $password) + { + $params = [ + 'name' => $user, + 'password' => $password, + ]; + + return $this->request(self::URI_AUTH_USER_ADD, $params); + } + + /** + * get detailed user information + * + * @param string $user + * @return array + * @throws \GuzzleHttp\Exception\BadResponseException + */ + public function getUser($user) + { + $params = [ + 'name' => $user, + ]; + + $body = $this->request(self::URI_AUTH_USER_GET, $params); + if ($this->pretty && isset($body['roles'])) { + return $body['roles']; + } + + return $body; + } + + /** + * delete a specified user + * + * @param string $user + * @return array + * @throws \GuzzleHttp\Exception\BadResponseException + */ + public function deleteUser($user) + { + $params = [ + 'name' => $user, + ]; + + $body = $this->request(self::URI_AUTH_USER_DELETE, $params); + + return $body; + } + + /** + * get a list of all users. + * + * @return array + * @throws \GuzzleHttp\Exception\BadResponseException + */ + public function userList() + { + $body = $this->request(self::URI_AUTH_USER_LIST); + if ($this->pretty && isset($body['users'])) { + return $body['users']; + } + + return $body; + } + + /** + * change the password of a specified user. + * + * @param string $user + * @param string $password + * @return array + * @throws \GuzzleHttp\Exception\BadResponseException + */ + public function changeUserPassword($user, $password) + { + $params = [ + 'name' => $user, + 'password' => $password, + ]; + + $body = $this->request(self::URI_AUTH_USER_CHANGE_PASSWORD, $params); + + return $body; + } + + /** + * grant a permission of a specified key or range to a specified role. + * + * @param string $role + * @param int $permType + * @param string $key + * @param string|null $rangeEnd + * @return array + * @throws \GuzzleHttp\Exception\BadResponseException + */ + public function grantRolePermission($role, $permType, $key, $rangeEnd = null) + { + $params = [ + 'name' => $role, + 'perm' => [ + 'permType' => $permType, + 'key' => base64_encode($key), + ], + ]; + if ($rangeEnd !== null) { + $params['perm']['range_end'] = base64_encode($rangeEnd); + } + + $body = $this->request(self::URI_AUTH_ROLE_GRANT, $params); + + return $body; + } + + /** + * revoke a key or range permission of a specified role. + * + * @param string $role + * @param string $key + * @param string|null $rangeEnd + * @return array + * @throws BadResponseException + */ + public function revokeRolePermission($role, $key, $rangeEnd = null) + { + $params = [ + 'role' => $role, + 'key' => $key, + ]; + if ($rangeEnd !== null) { + $params['range_end'] = $rangeEnd; + } + + $body = $this->request(self::URI_AUTH_ROLE_REVOKE, $params); + + return $body; + } + + /** + * grant a role to a specified user. + * + * @param string $user + * @param string $role + * @return array + * @throws BadResponseException + */ + public function grantUserRole($user, $role) + { + $params = [ + 'user' => $user, + 'role' => $role, + ]; + + $body = $this->request(self::URI_AUTH_USER_GRANT, $params); + + return $body; + } + + /** + * revoke a role of specified user. + * + * @param string $user + * @param string $role + * @return array + * @throws \GuzzleHttp\Exception\BadResponseException + */ + public function revokeUserRole($user, $role) + { + $params = [ + 'name' => $user, + 'role' => $role, + ]; + + $body = $this->request(self::URI_AUTH_USER_REVOKE, $params); + + return $body; + } + + // endregion auth + + /** + * 发送HTTP请求 + * + * @param string $method + * @param string $uri + * @param array $params 请求参数 + * @param array $options 可选参数 + * @return array + * @throws Exception + */ + protected function request(string $method, string $uri, array $params = [], array $options = []): array + { + if ($options) { + $params = array_merge($params, $options); + } + // 没有参数, 设置一个默认参数 + if (!$params) { + $params['php-etcd-client'] = 1; + } + $client = new CoroutineClient('47.92.194.207', 2379); + if (!empty($params)) { + $client->setHeaders(['Content-Type' => 'application/x-www-form-urlencoded']); + $client->setData(http_build_query($params)); + } + + $client->setMethod($method); + $client->execute($uri); + $body = json_decode($client->body, true); + $client->close(); + + if (!in_array($client->getStatusCode(), [200, 201])) { + throw new Exception($client->body, $client->errCode); + } + return $body; + } + + + /** + * string类型key用base64编码 + * + * @param array $data + * @return array + */ + protected function encode(array $data): array + { + + foreach ($data as $key => $value) { + if (is_string($value)) { + $data[$key] = base64_encode($value); + } + } + + return $data; + } + + /** + * 指定字段base64解码 + * + * @param array $body + * @param string $bodyKey + * @param array $fields 需要解码的字段 + * @return array + */ + protected function decodeBodyForFields(array $body, string $bodyKey, array $fields): array + { + if (!isset($body[$bodyKey])) { + return $body; + } + $data = $body[$bodyKey]; + if (!isset($data[0])) { + $data = array($data); + } + foreach ($data as $key => $value) { + foreach ($fields as $field) { + if (isset($value[$field])) { + $data[$key][$field] = base64_decode($value[$field]); + } + } + } + + if (isset($body[$bodyKey][0])) { + $body[$bodyKey] = $data; + } else { + $body[$bodyKey] = $data[0]; + } + + return $body; + } + + + /** + * @param array $data + * @return mixed + */ + protected function convertFields(array $data): mixed + { + if (!isset($data[0])) { + return $data['value']; + } + + $map = []; + foreach ($data as $value) { + $key = $value['key']; + $map[$key] = $value['value']; + } + + return $map; + } + +} diff --git a/src/Registry.php b/src/Registry.php new file mode 100644 index 0000000..ad2ecd4 --- /dev/null +++ b/src/Registry.php @@ -0,0 +1,51 @@ +