CompareBasketObject: new helper for basket diffs

...with lot's of workarounds for foreign baskets

fixes #2223
This commit is contained in:
Thomas Gelf 2020-11-24 04:36:48 +01:00
parent a4e2ba5fc7
commit a761485b96
3 changed files with 179 additions and 18 deletions

View File

@ -13,6 +13,7 @@ use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshotFieldResolver;
use Icinga\Module\Director\DirectorObject\Automation\CompareBasketObject;
use Icinga\Module\Director\Forms\AddToBasketForm;
use Icinga\Module\Director\Forms\BasketCreateSnapshotForm;
use Icinga\Module\Director\Forms\BasketForm;
@ -289,7 +290,7 @@ class BasketController extends ActionController
if (isset($object->originalId)) {
unset($object->originalId);
}
$hasChanged = Json::encode($currentExport) !== Json::encode($object);
$hasChanged = ! CompareBasketObject::equals($currentExport, $object);
$table->addNameValueRow(
$key,
$hasChanged
@ -303,7 +304,12 @@ class BasketController extends ActionController
} catch (Exception $e) {
$table->addNameValueRow(
$key,
$e->getMessage()
Html::tag('a', sprintf(
'%s (%s:%d)',
$e->getMessage(),
basename($e->getFile()),
$e->getLine()
))
);
}
}
@ -368,21 +374,30 @@ class BasketController extends ActionController
}
$fieldResolver = new BasketSnapshotFieldResolver($objects, $connection);
$objectFromBasket = $objects->$type->$key;
unset($objectFromBasket->originalId);
CompareBasketObject::normalize($objectFromBasket);
$objectFromBasket = Json::encode($objectFromBasket, JSON_PRETTY_PRINT);
$current = BasketSnapshot::instanceByIdentifier($type, $key, $connection);
if ($current === null) {
$current = '';
} else {
$exported = $current->export();
$fieldResolver->tweakTargetIds($exported);
unset($exported->originalId);
CompareBasketObject::normalize($exported);
$current = Json::encode($exported, JSON_PRETTY_PRINT);
}
$this->content()->add(
ConfigDiff::create(
$current,
Json::encode($objectFromBasket, JSON_PRETTY_PRINT)
)->setHtmlRenderer('Inline')
);
if ($current === $objectFromBasket) {
$this->content()->add([
Hint::ok('Basket equals current object'),
Html::tag('pre', $current)
]);
} else {
$this->content()->add(
ConfigDiff::create($current, $objectFromBasket)->setHtmlRenderer('Inline')
);
}
}
/**

View File

@ -0,0 +1,146 @@
<?php
namespace Icinga\Module\Director\DirectorObject\Automation;
use Icinga\Module\Director\Core\Json;
use ipl\Html\Error;
use RuntimeException;
use function array_key_exists;
use function is_array;
use function is_object;
use function is_scalar;
class CompareBasketObject
{
public static function normalize(&$value)
{
if (is_scalar($value)) {
return;
}
if (is_array($value)) {
foreach ($value as $k => &$v) {
static::normalize($v);
}
unset($v);
}
if (is_object($value)) {
$sorted = (array) $value;
// foreign baskets might not sort as we do:
ksort($sorted);
foreach ($sorted as $k => &$v) {
static::normalize($v);
}
unset($v);
$value = $sorted;
// foreign baskets might not sort those lists correctly:
if (isset($value->list_name) && isset($value->entries)) {
static::sortListBy('entry_name', $value->entries);
}
if (isset($value->fields)) {
static::sortListBy('datafield_id', $value->fields);
}
}
}
protected static function sortListBy($key, &$list)
{
usort($list, function ($a, $b) use ($key) {
return $a->$key > $b->$key;
});
}
public static function equals($a, $b)
{
if (is_scalar($a)) {
return $a === $b;
}
if ($a === null) {
return $b === null;
}
// Well... this is annoying :-/
$a = Json::decode(Json::encode($a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
$b = Json::decode(Json::encode($b, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
if (is_array($a)) {
// Empty arrays VS empty objects :-( This is a fallback, not needed unless en/decode takes place
if (empty($a) && is_object($b) && (array) $b === []) {
return true;
}
if (! is_array($b)) {
return false;
}
if (array_keys($a) !== array_keys($b)) {
return false;
}
foreach ($a as $k => $v) {
if (array_key_exists($k, $b) && static::equals($b[$k], $v)) {
continue;
}
return false;
}
return true;
}
if (is_object($a)) {
// Well... empty arrays VS empty objects :-(
if ($b === [] && (array) $a === []) {
return true;
}
if (! is_object($b)) {
return false;
}
// Workaround, same as above
if (isset($a->list_name) && isset($a->entries)) {
if (! isset($b->entries)) {
return false;
}
static::sortListBy('entry_name', $a->entries);
static::sortListBy('entry_name', $b->entries);
}
if (isset($a->fields) && isset($b->fields)) {
static::sortListBy('datafield_id', $a->fields);
static::sortListBy('datafield_id', $b->fields);
}
foreach ((array) $a as $k => $v) {
if (property_exists($b, $k) && static::equals($v, $b->$k)) {
continue;
}
if (! property_exists($b, $k)) {
if ($v === null) {
continue;
}
// Deal with two special defaults:
if ($k === 'set_if_format' && $v === 'string') {
continue;
}
if ($k === 'disabled' && $v === false) {
continue;
}
}
return false;
}
foreach ((array) $b as $k => $v) {
if (! property_exists($a, $k)) {
if ($v === null) {
continue;
}
// Once again:
if ($k === 'set_if_format' && $v === 'string') {
continue;
}
if ($k === 'disabled' && $v === false) {
continue;
}
return false;
}
}
return true;
}
throw new RuntimeException("Cannot compare " . Error::getPhpTypeName($a));
}
}

View File

@ -5,6 +5,7 @@ namespace Icinga\Module\Director\Objects;
use Icinga\Module\Director\Core\Json;
use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\CompareBasketObject;
use Icinga\Module\Director\Exception\DuplicateKeyException;
use Icinga\Module\Director\Forms\IcingaServiceForm;
use Icinga\Module\Director\Hook\DataTypeHook;
@ -159,15 +160,13 @@ class DirectorDatafield extends DbObjectWithSettings
$list = null;
}
$encoded = Json::encode($properties);
if ($id) {
if (static::exists($id, $db)) {
$existing = static::loadWithAutoIncId($id, $db);
$existingProperties = (array) $existing->export();
unset($existingProperties['originalId']);
if ($encoded === Json::encode($existingProperties)) {
return $existing;
}
$compare = Json::decode(Json::encode($properties));
if ($id && static::exists($id, $db)) {
$existing = static::loadWithAutoIncId($id, $db);
$existingProperties = (array) $existing->export();
unset($existingProperties['originalId']);
if (CompareBasketObject::equals((object) $compare, (object) $existingProperties)) {
return $existing;
}
}
@ -185,7 +184,8 @@ class DirectorDatafield extends DbObjectWithSettings
foreach ($candidates as $candidate) {
$export = $candidate->export();
unset($export->originalId);
if (Json::encode($export) === $encoded) {
CompareBasketObject::normalize($export);
if (CompareBasketObject::equals($export, $compare)) {
return $candidate;
}
}