diff --git a/library/Director/Data/ObjectImporter.php b/library/Director/Data/ObjectImporter.php new file mode 100644 index 00000000..57ec3953 --- /dev/null +++ b/library/Director/Data/ObjectImporter.php @@ -0,0 +1,166 @@ +db = $db; + } + + /** + * @param class-string|DbObject $implementation + * @param stdClass $plain + * @return DbObject + * @throws JsonDecodeException + */ + public function import(string $implementation, stdClass $plain): DbObject + { + $this->assertTemplate($implementation, $plain); + $this->fixRelations($implementation, $plain); + $this->applyOtherWorkarounds($implementation, $plain); + $this->fixLegacyBaskets($implementation, $plain); + $this->fixSubObjects($implementation, $plain); + + $object = $this->loadExistingObject($implementation, $plain); + if ($object === null) { + $object = $implementation::create([], $this->db); + } + + $properties = (array) $plain; + unset($properties['fields']); + unset($properties['originalId']); + if ($implementation === Basket::class) { + if (isset($properties['objects']) && is_string($properties['objects'])) { + $properties['objects'] = JsonString::decode($properties['objects']); + } + } + $object->setProperties($properties); + + return $object; + } + + protected function fixLegacyBaskets(string $implementation, stdClass $plain) + { + // TODO: Check, whether current export sets modifiers = [] in case there is none + if ($implementation == ImportSource::class) { + if (!isset($plain->modifiers)) { + $plain->modifiers = []; + } + } + } + + protected function applyOtherWorkarounds(string $implementation, stdClass $plain) + { + if ($implementation === SyncRule::class) { + if (isset($plain->properties)) { + $plain->syncProperties = $plain->properties; + unset($plain->properties); + } + } + } + + protected function fixSubObjects(string $implementation, stdClass $plain) + { + if ($implementation === IcingaServiceSet::class) { + foreach ($plain->services as $service) { + unset($service->fields); + } + // Hint: legacy baskets are carrying service names as object keys, new baskets have arrays + $plain->services = array_values((array) $plain->services); + } + } + + protected function fixRelations(string $implementation, stdClass $plain) + { + if ($implementation === DirectorJob::class) { + $settings = $plain->settings; + $source = $settings->source ?? null; + if ($source && !isset($settings->source_id)) { + $settings->source_id = ImportSource::load($source, $this->db)->get('id'); + unset($settings->source); + } + $rule = $settings->rule ?? null; + if ($rule && !isset($settings->rule_id)) { + $settings->rule_id = SyncRule::load($rule, $this->db)->get('id'); + unset($settings->rule); + } + } + } + + /** + * @param class-string $implementation + * @param stdClass $plain + * @return DbObject|null + */ + protected function loadExistingObject(string $implementation, stdClass $plain): ?DbObject + { + if (isset($plain->uuid)) { + return $implementation::loadWithUniqueId(Uuid::fromString($plain->uuid), $this->db); + } + + if ($implementation === IcingaService::class) { + $key = [ + 'object_type' => 'template', + 'object_name' => $plain->object_name + ]; + } else { + $dummy = $implementation::create(); + $keyColumn = $dummy->getKeyName(); + if (is_array($keyColumn)) { + if (empty($keyColumn)) { + throw new \RuntimeException("$implementation has an empty keyColumn array"); + } + $key = []; + foreach ($keyColumn as $column) { + if (isset($plain->$column)) { + $key[$column] = $plain->$column; + } + } + } else { + $key = $plain->$keyColumn; + } + } + + return $implementation::loadOptional($key, $this->db); + } + + protected function assertTemplate(string $implementation, stdClass $plain) + { + if (! in_array($implementation, self::$templatesOnly)) { + return; + } + if ($plain->object_type !== 'template') { + throw new InvalidArgumentException(sprintf( + 'Can import only Templates, got "%s" for "%s"', + $plain->object_type, + $plain->name + )); + } + } +} diff --git a/library/Director/DirectorObject/Automation/BasketSnapshot.php b/library/Director/DirectorObject/Automation/BasketSnapshot.php index 4ddf2cef..def4038e 100644 --- a/library/Director/DirectorObject/Automation/BasketSnapshot.php +++ b/library/Director/DirectorObject/Automation/BasketSnapshot.php @@ -2,10 +2,11 @@ namespace Icinga\Module\Director\DirectorObject\Automation; +use gipfl\Json\JsonDecodeException; use gipfl\Json\JsonEncodeException; use gipfl\Json\JsonString; -use Icinga\Module\Director\Core\Json; use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\Data\ObjectImporter; use Icinga\Module\Director\Db; use Icinga\Module\Director\Data\Db\DbObject; use Icinga\Module\Director\Objects\DirectorDatafield; @@ -30,6 +31,7 @@ use Icinga\Module\Director\Objects\ImportSource; use Icinga\Module\Director\Objects\SyncRule; use InvalidArgumentException; use RuntimeException; +use stdClass; class BasketSnapshot extends DbObject { @@ -240,7 +242,7 @@ class BasketSnapshot extends DbObject 'basket_uuid' => $basket->get('uuid') ]); $snapshot->objects = []; - foreach ((array) Json::decode($string) as $type => $objects) { + foreach ((array) JsonString::decode($string) as $type => $objects) { $snapshot->objects[$type] = (array) $objects; } @@ -251,21 +253,19 @@ class BasketSnapshot extends DbObject { $snapshot = new static(); $snapshot->restoreObjects( - Json::decode($string), + JsonString::decode($string), $connection, $replace ); } /** - * @param $all - * @param Db $connection - * @param bool $replace * @throws \Icinga\Module\Director\Exception\DuplicateKeyException * @throws \Zend_Db_Adapter_Exception * @throws \Icinga\Exception\NotFoundError + * @throws JsonDecodeException */ - protected function restoreObjects($all, Db $connection, $replace = true) + protected function restoreObjects(stdClass $all, Db $connection, $replace = true) { $db = $connection->getDbAdapter(); $db->beginTransaction(); @@ -280,21 +280,17 @@ class BasketSnapshot extends DbObject } /** - * @param $all - * @param $typeName - * @param BasketSnapshotFieldResolver $fieldResolver - * @param Db $connection - * @param $replace * @throws \Icinga\Exception\NotFoundError * @throws \Icinga\Module\Director\Exception\DuplicateKeyException * @throws \Zend_Db_Adapter_Exception + * @throws JsonDecodeException */ public function restoreType( - &$all, - $typeName, + stdClass $all, + string $typeName, BasketSnapshotFieldResolver $fieldResolver, Db $connection, - $replace + bool $replace ) { if (isset($all->$typeName)) { $objects = (array) $all->$typeName; @@ -302,11 +298,10 @@ class BasketSnapshot extends DbObject return; } $class = static::getClassForType($typeName); - + $importer = new ObjectImporter($connection); $changed = []; - foreach ($objects as $key => $object) { - /** @var DbObject $new */ - $new = $class::import($object, $connection, $replace); + foreach ($objects as $object) { + $new = $importer->import($class, $object); if ($new->hasBeenModified()) { if ($new instanceof IcingaObject && $new->supportsImports()) { /** @var ExportInterface $new */ @@ -325,7 +320,6 @@ class BasketSnapshot extends DbObject $fieldResolver->relinkObjectFields($new, $object); } } - $allObjects[spl_object_hash($new)] = $object; } /** @var IcingaObject $object */ @@ -334,7 +328,7 @@ class BasketSnapshot extends DbObject } foreach ($changed as $key => $new) { // Store related fields. As objects might have formerly been - // un-stored, let's to it right here + // un-stored, let's do it right here if ($new instanceof IcingaObject) { $fieldResolver->relinkObjectFields($new, $objects[$key]); } @@ -358,10 +352,9 @@ class BasketSnapshot extends DbObject } /** - * @return BasketContent * @throws \Icinga\Exception\NotFoundError */ - protected function getContent() + protected function getContent(): BasketContent { if ($this->content === null) { $this->content = BasketContent::load($this->get('content_checksum'), $this->getConnection()); @@ -380,26 +373,25 @@ class BasketSnapshot extends DbObject } /** - * @return string - * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\NotFoundError|JsonEncodeException */ - public function getJsonSummary() + public function getJsonSummary(): string { if ($this->hasBeenLoadedFromDb()) { return $this->getContent()->get('summary'); } - return Json::encode($this->getSummary(), JSON_PRETTY_PRINT); + return JsonString::encode($this->getSummary(), JSON_PRETTY_PRINT); } /** * @return array|mixed - * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\NotFoundError|JsonDecodeException */ public function getSummary() { if ($this->hasBeenLoadedFromDb()) { - return Json::decode($this->getContent()->get('summary')); + return JsonString::decode($this->getContent()->get('summary')); } $summary = []; @@ -412,7 +404,7 @@ class BasketSnapshot extends DbObject /** * @return string - * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\NotFoundError|JsonEncodeException */ public function getJsonDump() { @@ -493,21 +485,17 @@ class BasketSnapshot extends DbObject */ public static function instanceByIdentifier($typeName, $identifier, Db $connection) { + /** @var class-string $class */ $class = static::getClassForType($typeName); - if (substr($class, -13) === 'IcingaService') { + if ($class === IcingaService::class) { $identifier = [ 'object_type' => 'template', 'object_name' => $identifier, ]; } - /** @var ExportInterface $object */ - if ($class::exists($identifier, $connection)) { - $object = $class::load($identifier, $connection); - } else { - $object = null; - } - return $object; + /** @var ExportInterface $object */ + return $class::loadOptional($identifier, $connection); } /**