ObjectImporter: new centralized importer

fixes #2714
This commit is contained in:
Thomas Gelf 2023-03-07 12:09:42 +01:00
parent b4839f6855
commit c24961f209
2 changed files with 192 additions and 38 deletions

View File

@ -0,0 +1,166 @@
<?php
namespace Icinga\Module\Director\Data;
use gipfl\Json\JsonDecodeException;
use gipfl\Json\JsonString;
use Icinga\Module\Director\Data\Db\DbObject;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use Icinga\Module\Director\Objects\DirectorJob;
use Icinga\Module\Director\Objects\IcingaHost;
use Icinga\Module\Director\Objects\IcingaService;
use Icinga\Module\Director\Objects\IcingaServiceSet;
use Icinga\Module\Director\Objects\ImportSource;
use Icinga\Module\Director\Objects\SyncRule;
use InvalidArgumentException;
use Ramsey\Uuid\Uuid;
use stdClass;
class ObjectImporter
{
protected static $templatesOnly = [
IcingaHost::class,
IcingaService::class,
IcingaServiceSet::class,
];
/** @var Db */
protected $db;
public function __construct(Db $db)
{
$this->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<DbObject> $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
));
}
}
}

View File

@ -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<DbObject> $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);
}
/**