Skip to content

Commit 9dfd25f

Browse files
committed
massive update allowing watches to support non-associative array responses
Signed-off-by: Travis Glenn Hansen <travisghansen@yahoo.com>
1 parent 0e008b7 commit 9dfd25f

File tree

7 files changed

+240
-19
lines changed

7 files changed

+240
-19
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ composer.lock
1717
/php-cs-fixer.phar
1818
.php_cs.cache
1919

20+
/dev
2021

2122
# always keep .keep files around
2223
!.keep

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# v0.4.0
2+
3+
Released 2023-10-09
4+
5+
- better support for `pcntl` signal handling
6+
- more control over how requests / responses are handled (allow control of encode/decoding options)
7+
- support for `ReactPHP` loops
8+
- small internal library (`Dotty`) useful for interacting with structured data (arrays, stdobject)
9+
- update composer deps

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
"require": {
1414
"php": ">=7.2.0",
1515
"ext-json": "*",
16-
"symfony/yaml": "^5.0",
17-
"flow/jsonpath": "^0.5.0"
16+
"symfony/yaml": "^6.3",
17+
"softcreatr/jsonpath": "^0.8.3"
1818
},
1919
"suggest": {
2020
"ext-pcntl": "support forking of watches",

src/KubernetesClient/Client.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ public function setDefaultRequestOptions($options) {
221221
* @param $options
222222
* @return mixed|void
223223
*/
224-
protected function getRequestOption($option, $options) {
224+
public function getRequestOption($option, $options) {
225225
$defaults = [
226226
'encode_flags' => 0,
227227
'decode_flags' => 0,

src/KubernetesClient/Config.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public static function shutdown()
177177
* Otherwise try to create a config based off running inside a cluster if corresponding files found.
178178
*
179179
* @return Config
180-
* @throws \Error If no config can be found at at all the default paths.
180+
* @throws \Error If no config can be found at all the default paths.
181181
*/
182182
public static function LoadFromDefault()
183183
{
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<?php
2+
3+
namespace KubernetesClient\Dotty;
4+
5+
class DotAccess {
6+
private const DELIMITERS = ['.'];
7+
protected static function keyToPathArray($path): array
8+
{
9+
if (is_array($path)) {
10+
return $path;
11+
}
12+
13+
if (\strlen($path) === 0) {
14+
throw new \Exception('Path cannot be an empty string');
15+
}
16+
17+
$path = \str_replace(self::DELIMITERS, '.', $path);
18+
$path = preg_replace('/\[([a-zA-Z0-9_\-]*)\]/', '.${1}', $path);
19+
20+
return \explode('.', $path);
21+
}
22+
23+
public static function propExists(&$data, $key) {
24+
if (is_object($data) && property_exists($data, $key)) {
25+
return true;
26+
}
27+
if (is_array($data) && array_key_exists($key, $data)) {
28+
return true;
29+
}
30+
31+
return false;
32+
}
33+
public static function &propGet(&$data, $key) {
34+
if (!self::propExists($data, $key)) {
35+
throw new \Exception('property not present');
36+
}
37+
if (is_object($data)) {
38+
return $data->{$key};
39+
}
40+
if (is_array($data)) {
41+
return $data[$key];
42+
}
43+
}
44+
45+
public static function propSet(&$data, $key, $value) {
46+
if (is_object($data)) {
47+
$data->{$key} = $value;
48+
}
49+
if (is_array($data)) {
50+
$data[$key] = $value;
51+
}
52+
}
53+
54+
public static function isStructuredData(&$data) {
55+
if (is_object($data) || is_array($data)) {
56+
return true;
57+
}
58+
return false;
59+
}
60+
61+
public static function &get(&$currentValue, $key, $default = null) {
62+
$hasDefault = \func_num_args() > 2;
63+
64+
if (is_string($key)) {
65+
$keyPath = self::keyToPathArray($key);
66+
} else {
67+
$keyPath = $key;
68+
}
69+
70+
foreach ($keyPath as $currentKey) {
71+
if (!self::isStructuredData($currentValue) || !self::propExists($currentValue, $currentKey)) {
72+
if ($hasDefault) {
73+
return $default;
74+
}
75+
76+
throw new \Exception('path not present');
77+
}
78+
79+
$currentValue = &self::propGet($currentValue, $currentKey);
80+
}
81+
82+
return $currentValue === null ? $default : $currentValue;
83+
}
84+
85+
public static function exists(&$currentValue, $key) {
86+
if (is_string($key)) {
87+
$keyPath = self::keyToPathArray($key);
88+
} else {
89+
$keyPath = $key;
90+
}
91+
92+
foreach ($keyPath as $currentKey) {
93+
if (!self::isStructuredData($currentValue) || !self::propExists($currentValue, $currentKey)) {
94+
return false;
95+
}
96+
97+
$currentValue = &self::propGet($currentValue, $currentKey);
98+
}
99+
100+
return true;
101+
}
102+
103+
public static function set(&$data, $key, $value, $options = []) {
104+
if (is_string($key)) {
105+
$keyPath = self::keyToPathArray($key);
106+
} else {
107+
$keyPath = $key;
108+
}
109+
110+
$currentValue = &$data;
111+
112+
$keySize = sizeof($keyPath);
113+
for ($i = 0; $i < $keySize; $i++) {
114+
$currentKey = $keyPath[$i];
115+
116+
if ($i == ($keySize - 1)) {
117+
self::propSet($currentValue, $currentKey, $value);
118+
return;
119+
}
120+
121+
if (!self::isStructuredData($currentValue) && self::propExists($currentValue, $currentKey)) {
122+
throw new \Exception("key {$currentKey} already exists but it unstructured content");
123+
}
124+
125+
if (!self::propExists($currentValue, $currentKey)) {
126+
// if option to create is enabled create
127+
if (self::get($options, 'create_structure', true)) {
128+
$type = self::get($options, 'create_structure_type', 'obj');
129+
if ($type == 'array') {
130+
self::propSet($currentValue, $currentKey, []);
131+
}
132+
133+
if ($type == 'obj') {
134+
self::propSet($currentValue, $currentKey, new \stdClass());
135+
}
136+
} else {
137+
throw new \Exception('necessary parents do not exist');
138+
}
139+
}
140+
141+
$currentValue = &self::propGet($currentValue, $currentKey);
142+
}
143+
}
144+
}

src/KubernetesClient/Watch.php

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace KubernetesClient;
44

5+
use KubernetesClient\Dotty\DotAccess;
6+
57
/**
68
* Used for the various kubernetes watch endpoints for continuous feed of data
79
*
@@ -201,6 +203,7 @@ private function getHandle()
201203
$handle = @fopen($url, 'r', false, $this->getClient()->getStreamContext());
202204
if ($handle === false) {
203205
$e = error_get_last();
206+
var_dump($e);
204207
throw new \Exception($e['message'], $e['type']);
205208
}
206209
stream_set_timeout($handle, 0, $this->getStreamTimeout());
@@ -408,7 +411,22 @@ private function internal_iterator($cycles = 0)
408411
{
409412
$handle = $this->getHandle();
410413
$i_cycles = 0;
414+
415+
$associative = $this->getClient()->getRequestOption('decode_associative', []);
416+
$decode_flags = $this->getClient()->getRequestOption('decode_flags', []);
417+
$decode_response = $this->getClient()->getRequestOption('decode_response', []);
418+
419+
/**
420+
* Mitigation for improper ordering especially during initial load
421+
* This acts as a tripwire, once tripped it should never go back to false
422+
*
423+
* https://github.com/kubernetes/kubernetes/issues/49745
424+
*/
425+
$initial_load_finished = false;
411426
while (true) {
427+
if (function_exists('pcntl_signal_dispatch')) {
428+
\pcntl_signal_dispatch();
429+
}
412430
if ($this->getStop()) {
413431
$this->internal_stop();
414432
return;
@@ -426,7 +444,7 @@ private function internal_iterator($cycles = 0)
426444

427445
//$meta = stream_get_meta_data($handle);
428446
if (feof($handle)) {
429-
if ($this->params['timeoutSeconds'] > 0) {
447+
if (key_exists('timeoutSeconds', $this->params) && $this->params['timeoutSeconds'] > 0) {
430448
//assume we've reached a successful end of watch
431449
return;
432450
} else {
@@ -449,6 +467,10 @@ private function internal_iterator($cycles = 0)
449467
$this->setLastBytesReadTimestamp(time());
450468
}
451469

470+
if (!$initial_load_finished && empty($data)) {
471+
$initial_load_finished = true;
472+
}
473+
452474
$this->buffer .= $data;
453475

454476
//break immediately if nothing is on the buffer
@@ -462,7 +484,7 @@ private function internal_iterator($cycles = 0)
462484
for ($x = 0; $x < ($parts_count - 1); $x++) {
463485
if (!empty($parts[$x])) {
464486
try {
465-
$response = json_decode($parts[$x], true);
487+
$response = json_decode($parts[$x], $associative, 512, $decode_flags);
466488
$code = $this->preProcessResponse($response);
467489
if ($code != 0) {
468490
$this->resetHandle();
@@ -471,12 +493,24 @@ private function internal_iterator($cycles = 0)
471493
goto end;
472494
}
473495

474-
if ($response['object']['metadata']['resourceVersion'] > $this->getResourceVersionLastSuccess()) {
475-
($this->callback)($response, $this);
496+
if (!$initial_load_finished && DotAccess::get($response, 'type') != "ADDED") {
497+
$initial_load_finished = true;
476498
}
477499

478-
$this->setResourceVersion($response['object']['metadata']['resourceVersion']);
479-
$this->setResourceVersionLastSuccess($response['object']['metadata']['resourceVersion']);
500+
$rv = DotAccess::get($response, 'object.metadata.resourceVersion');
501+
502+
if (!$initial_load_finished || $rv > $this->getResourceVersionLastSuccess()) {
503+
if (!$decode_response) {
504+
($this->callback)($parts[$x], $this);
505+
} else {
506+
($this->callback)($response, $this);
507+
}
508+
}
509+
510+
if ($rv > $this->getResourceVersionLastSuccess()) {
511+
$this->setResourceVersion($rv);
512+
$this->setResourceVersionLastSuccess($rv);
513+
}
480514

481515
if ($this->getStop()) {
482516
$this->internal_stop();
@@ -509,7 +543,23 @@ private function internal_generator($cycles = 0)
509543
{
510544
$handle = $this->getHandle();
511545
$i_cycles = 0;
546+
547+
$associative = $this->getClient()->getRequestOption('decode_associative', []);
548+
$decode_flags = $this->getClient()->getRequestOption('decode_flags', []);
549+
$decode_response = $this->getClient()->getRequestOption('decode_response', []);
550+
551+
/**
552+
* Mitigation for improper ordering especially during initial load
553+
* This acts as a tripwire, once tripped it should never go back to false
554+
*
555+
* https://github.com/kubernetes/kubernetes/issues/49745
556+
*/
557+
$initial_load_finished = false;
512558
while (true) {
559+
if (function_exists('pcntl_signal_dispatch')) {
560+
\pcntl_signal_dispatch();
561+
}
562+
513563
if ($this->getStop()) {
514564
$this->internal_stop();
515565
return;
@@ -527,7 +577,7 @@ private function internal_generator($cycles = 0)
527577

528578
//$meta = stream_get_meta_data($handle);
529579
if (feof($handle)) {
530-
if ($this->params['timeoutSeconds'] > 0) {
580+
if (key_exists('timeoutSeconds', $this->params) && $this->params['timeoutSeconds'] > 0) {
531581
//assume we've reached a successful end of watch
532582
return;
533583
} else {
@@ -550,6 +600,10 @@ private function internal_generator($cycles = 0)
550600
$this->setLastBytesReadTimestamp(time());
551601
}
552602

603+
if (!$initial_load_finished && empty($data)) {
604+
$initial_load_finished = true;
605+
}
606+
553607
$this->buffer .= $data;
554608

555609
//break immediately if nothing is on the buffer
@@ -563,7 +617,7 @@ private function internal_generator($cycles = 0)
563617
for ($x = 0; $x < ($parts_count - 1); $x++) {
564618
if (!empty($parts[$x])) {
565619
try {
566-
$response = json_decode($parts[$x], true);
620+
$response = json_decode($parts[$x], $associative, 512, $decode_flags);
567621
$code = $this->preProcessResponse($response);
568622
if ($code != 0) {
569623
$this->resetHandle();
@@ -572,13 +626,26 @@ private function internal_generator($cycles = 0)
572626
goto end;
573627
}
574628

575-
$yield = ($response['object']['metadata']['resourceVersion'] > $this->getResourceVersionLastSuccess());
629+
if (!$initial_load_finished && DotAccess::get($response, 'type') != "ADDED") {
630+
$initial_load_finished = true;
631+
}
632+
633+
$rv = DotAccess::get($response, 'object.metadata.resourceVersion');
576634

577-
$this->setResourceVersion($response['object']['metadata']['resourceVersion']);
578-
$this->setResourceVersionLastSuccess($response['object']['metadata']['resourceVersion']);
635+
// https://github.com/kubernetes/kubernetes/issues/49745
636+
$yield = (!$initial_load_finished || $rv > $this->getResourceVersionLastSuccess());
637+
638+
if ($rv > $this->getResourceVersionLastSuccess()) {
639+
$this->setResourceVersion($rv);
640+
$this->setResourceVersionLastSuccess($rv);
641+
}
579642

580643
if ($yield) {
581-
yield $response;
644+
if (!$decode_response) {
645+
yield $parts[$x];
646+
} else {
647+
yield $response;
648+
}
582649
}
583650

584651
if ($this->getStop()) {
@@ -603,16 +670,16 @@ private function internal_generator($cycles = 0)
603670

604671
private function preProcessResponse($response)
605672
{
606-
if (!is_array($response)) {
673+
if (!DotAccess::isStructuredData($response)) {
607674
return 1;
608675
}
609676

610-
if (key_exists('kind', $response) && $response['kind'] == 'Status' && $response['status'] == 'Failure') {
677+
if(DotAccess::get($response, 'kind', null) == 'Status' && DotAccess::get($response, 'status', null) == 'Failure') {
611678
return 1;
612679
}
613680

614681
// resourceVersion is too old
615-
if ($response['type'] == 'ERROR' && $response['object']['code'] == 410) {
682+
if (DotAccess::get($response, 'type', null) == 'ERROR' && DotAccess::get($response, 'object.code', null) == 410) {
616683
return 1;
617684
}
618685

0 commit comments

Comments
 (0)