IcingaConfigHelper: granular macro-rendering

fixes #685
fixes #1272
fixes #1482
This commit is contained in:
Thomas Gelf 2018-06-04 17:46:47 +02:00
parent c5d05454ca
commit b475aa841e
5 changed files with 149 additions and 76 deletions

View File

@ -46,7 +46,7 @@ class CustomVariableString extends CustomVariable
public function toConfigString($renderExpressions = false) public function toConfigString($renderExpressions = false)
{ {
if ($renderExpressions) { if ($renderExpressions) {
return c::renderStringWithVariables($this->getValue()); return c::renderStringWithVariables($this->getValue(), ['config']);
} else { } else {
return c::renderString($this->getValue()); return c::renderString($this->getValue());
} }

View File

@ -2,8 +2,7 @@
namespace Icinga\Module\Director\IcingaConfig; namespace Icinga\Module\Director\IcingaConfig;
use Icinga\Exception\IcingaException; use InvalidArgumentException;
use Icinga\Exception\ProgrammingError;
class IcingaConfigHelper class IcingaConfigHelper
{ {
@ -11,7 +10,7 @@ class IcingaConfigHelper
* Reserved words according to * Reserved words according to
* https://docs.icinga.com/icinga2/snapshot/doc/module/icinga2/chapter/language-reference#reserved-keywords * https://docs.icinga.com/icinga2/snapshot/doc/module/icinga2/chapter/language-reference#reserved-keywords
*/ */
protected static $reservedWords = array( protected static $reservedWords = [
'object', 'object',
'template', 'template',
'include', 'include',
@ -39,7 +38,7 @@ class IcingaConfigHelper
'in', 'in',
'current_filename', 'current_filename',
'current_line', 'current_line',
); ];
public static function renderKeyValue($key, $value, $prefix = ' ') public static function renderKeyValue($key, $value, $prefix = ' ')
{ {
@ -69,7 +68,10 @@ class IcingaConfigHelper
} elseif ($value === 'n' || $value === false) { } elseif ($value === 'n' || $value === false) {
return 'false'; return 'false';
} else { } else {
throw new ProgrammingError('%s is not a valid boolean', $value); throw new InvalidArgumentException(sprintf(
'%s is not a valid boolean',
$value
));
} }
} }
@ -98,7 +100,7 @@ class IcingaConfigHelper
// Parameter? Dedicated method? Always if \n is found? // Parameter? Dedicated method? Always if \n is found?
public static function renderString($string) public static function renderString($string)
{ {
$special = array( $special = [
'/\\\/', '/\\\/',
'/"/', '/"/',
'/\$/', '/\$/',
@ -106,10 +108,10 @@ class IcingaConfigHelper
'/\r/', '/\r/',
'/\n/', '/\n/',
// '/\b/', -> doesn't work // '/\b/', -> doesn't work
'/\f/' '/\f/',
); ];
$replace = array( $replace = [
'\\\\\\', '\\\\\\',
'\\"', '\\"',
'\\$', '\\$',
@ -118,7 +120,7 @@ class IcingaConfigHelper
'\\n', '\\n',
// '\\b', // '\\b',
'\\f', '\\f',
); ];
$string = preg_replace($special, $replace, $string); $string = preg_replace($special, $replace, $string);
@ -144,7 +146,10 @@ class IcingaConfigHelper
} elseif (is_string($value)) { } elseif (is_string($value)) {
return static::renderString($value); return static::renderString($value);
} else { } else {
throw new IcingaException('Unexpected type %s', var_export($value, 1)); throw new InvalidArgumentException(sprintf(
'Unexpected type %s',
var_export($value, 1)
));
} }
} }
@ -160,7 +165,7 @@ class IcingaConfigHelper
// Requires an array // Requires an array
public static function renderArray($array) public static function renderArray($array)
{ {
$data = array(); $data = [];
foreach ($array as $entry) { foreach ($array as $entry) {
if ($entry instanceof IcingaConfigRenderer) { if ($entry instanceof IcingaConfigRenderer) {
$data[] = $entry; $data[] = $entry;
@ -186,7 +191,7 @@ class IcingaConfigHelper
public static function renderDictionary($dictionary) public static function renderDictionary($dictionary)
{ {
$vals = array(); $vals = [];
foreach ($dictionary as $key => $value) { foreach ($dictionary as $key => $value) {
$vals[$key] = rtrim( $vals[$key] = rtrim(
self::renderKeyValue( self::renderKeyValue(
@ -259,11 +264,12 @@ class IcingaConfigHelper
$value = 0; $value = 0;
foreach ($parts as $part) { foreach ($parts as $part) {
if (! preg_match('/^(\d+)([dhms]?)$/', $part, $m)) { if (! preg_match('/^(\d+)([dhms]?)$/', $part, $m)) {
throw new ProgrammingError( throw new InvalidArgumentException(sprintf(
'"%s" is not a valid time (duration) definition', '"%s" is not a valid time (duration) definition',
$interval $interval
); ));
} }
switch ($m[2]) { switch ($m[2]) {
case 'd': case 'd':
$value += $m[1] * 86400; $value += $m[1] * 86400;
@ -290,11 +296,11 @@ class IcingaConfigHelper
return '0s'; return '0s';
} }
$steps = array( $steps = [
'd' => 86400, 'd' => 86400,
'h' => 3600, 'h' => 3600,
'm' => 60, 'm' => 60,
); ];
foreach ($steps as $unit => $duration) { foreach ($steps as $unit => $duration) {
if ($seconds % $duration === 0) { if ($seconds % $duration === 0) {
@ -305,37 +311,89 @@ class IcingaConfigHelper
return $seconds . 's'; return $seconds . 's';
} }
public static function stringHasMacro($string) public static function stringHasMacro($string, $macroName = null)
{ {
return preg_match('/(?<!\$)\$[\w\.]+\$(?!\$)/', $string); $len = strlen($string);
$start = false;
// TODO: robust UTF8 support. It works, but it is not 100% correct
for ($i = 0; $i < $len; $i++) {
if ($string[$i] === '$') {
if ($start === false) {
$start = $i;
} else {
// Escaping, $$
if ($start + 1 === $i) {
$start = false;
} else {
if ($macroName === null) {
return true;
} else {
if ($macroName === substr($string, $start + 1, $i - $start - 1)) {
return true;
} else {
$start = false;
}
}
}
}
}
}
return false;
} }
public static function renderStringWithVariables($string) /**
* Hint: this isn't complete, but let's restrict ourselves right now
*
* @param $name
* @return bool
*/
public static function isValidMacroName($name)
{ {
$string = preg_replace( return preg_match('/^[A-z_][A-z_\.\d]+$/', $name)
'/(?<!\$)\$([\w\.]+)\$(?!\$)/', && ! preg_match('/\.$/', $name);
'" + ${1} + "', }
static::renderString($string)
);
// TODO: this is an exemption for special variables. It would public static function renderStringWithVariables($string, array $whiteList = null)
// never make any sense to evaluate them at parse time. {
// Another issue remains: there might be other reasons $len = strlen($string);
// for "late evaluation". But how to distinguish those $start = false;
// use cases? $parts = [];
$string = preg_replace( // TODO: UTF8...
'/" \+ ((?:user|notification)\.[\w\.]+) \+ "/', $offset = 0;
'\$${1}\$', for ($i = 0; $i < $len; $i++) {
$string if ($string[$i] === '$') {
); if ($start === false) {
$start = $i;
} else {
// Ignore $$
if ($start + 1 === $i) {
$start = false;
} else {
// We got a macro
$macroName = substr($string, $start + 1, $i - $start - 1);
if (static::isValidMacroName($macroName)) {
if ($whiteList === null || in_array($macroName, $whiteList)) {
if ($start > $offset) {
$parts[] = static::renderString(
substr($string, $offset, $start - $offset)
);
}
$parts[] = $macroName;
$offset = $i + 1;
}
}
if (substr($string, 0, 5) === '"" + ') { $start = false;
$string = substr($string, 5); }
} }
if (substr($string, -5) === ' + ""') { }
$string = substr($string, 0, -5);
} }
return $string; if ($offset < $i) {
$parts[] = static::renderString(substr($string, $offset, $i - $offset));
}
return implode(' + ', $parts);
} }
} }

View File

@ -2,7 +2,7 @@
namespace Icinga\Module\Director\IcingaConfig; namespace Icinga\Module\Director\IcingaConfig;
use Icinga\Exception\ProgrammingError; use InvalidArgumentException;
class IcingaConfigRendered implements IcingaConfigRenderer class IcingaConfigRendered implements IcingaConfigRenderer
{ {
@ -11,7 +11,7 @@ class IcingaConfigRendered implements IcingaConfigRenderer
public function __construct($string) public function __construct($string)
{ {
if (! is_string($string)) { if (! is_string($string)) {
throw new ProgrammingError('IcingaConfigRendered accepts only strings'); throw new InvalidArgumentException('IcingaConfigRendered accepts only strings');
} }
$this->rendered = $string; $this->rendered = $string;

View File

@ -15,15 +15,12 @@ class CustomVariablesTest extends BaseTestCase
$vars->bla = 'da'; $vars->bla = 'da';
$vars->{'aBc'} = 'normal'; $vars->{'aBc'} = 'normal';
$vars->{'a-0'} = 'special'; $vars->{'a-0'} = 'special';
$expected = $this->indentVarsList(array( $expected = $this->indentVarsList([
'vars["a-0"] = "special"', 'vars["a-0"] = "special"',
'vars.aBc = "normal"', 'vars.aBc = "normal"',
'vars.bla = "da"' 'vars.bla = "da"'
)); ]);
$this->assertEquals( $this->assertEquals($expected, $vars->toConfigString());
$vars->toConfigString(),
$expected
);
} }
public function testVarsCanBeUnsetAndSetAgain() public function testVarsCanBeUnsetAndSetAgain()
@ -33,24 +30,21 @@ class CustomVariablesTest extends BaseTestCase
unset($vars->one); unset($vars->one);
$vars->one = 'three'; $vars->one = 'three';
$res = array(); $res = [];
foreach ($vars as $k => $v) { foreach ($vars as $k => $v) {
$res[$k] = $v->getValue(); $res[$k] = $v->getValue();
} }
$this->assertEquals( $this->assertEquals(['one' => 'three'], $res);
array('one' => 'three'),
$res
);
} }
public function testNumericKeysAreRenderedWithArraySyntax() public function testNumericKeysAreRenderedWithArraySyntax()
{ {
$vars = $this->newVars(); $vars = $this->newVars();
$vars->{'1'} = 1; $vars->{'1'} = 1;
$expected = $this->indentVarsList(array( $expected = $this->indentVarsList([
'vars["1"] = 1' 'vars["1"] = 1'
)); ]);
$this->assertEquals( $this->assertEquals(
$expected, $expected,
@ -63,14 +57,11 @@ class CustomVariablesTest extends BaseTestCase
$vars = $this->newVars(); $vars = $this->newVars();
$vars->bla = 'da'; $vars->bla = 'da';
$vars->abc = '$val$'; $vars->abc = '$val$';
$expected = $this->indentVarsList(array( $expected = $this->indentVarsList([
'vars.abc = val', 'vars.abc = "$val$"',
'vars.bla = "da"' 'vars.bla = "da"'
)); ]);
$this->assertEquals( $this->assertEquals($expected, $vars->toConfigString(true));
$vars->toConfigString(true),
$expected
);
} }
protected function indentVarsList($vars) protected function indentVarsList($vars)

View File

@ -19,7 +19,7 @@ class IcingaConfigHelperTest extends BaseTestCase
} }
/** /**
* @expectedException \Icinga\Exception\ProgrammingError * @expectedException \InvalidArgumentException
*/ */
public function testWhetherInvalidIntervalStringRaisesException() public function testWhetherInvalidIntervalStringRaisesException()
{ {
@ -53,12 +53,12 @@ class IcingaConfigHelperTest extends BaseTestCase
public function testWhetherDictionaryRendersCorrectly() public function testWhetherDictionaryRendersCorrectly()
{ {
$dict = (object) array( $dict = (object) [
'key1' => 'bla', 'key1' => 'bla',
'include' => 'reserved', 'include' => 'reserved',
'spe cial' => 'value', 'spe cial' => 'value',
'0' => 'numeric', '0' => 'numeric',
); ];
$this->assertEquals( $this->assertEquals(
c::renderDictionary($dict), c::renderDictionary($dict),
rtrim($this->loadRendered('dict1')) rtrim($this->loadRendered('dict1'))
@ -81,26 +81,50 @@ class IcingaConfigHelperTest extends BaseTestCase
$this->assertEquals(c::renderString('\f'), '"\\\\f"'); $this->assertEquals(c::renderString('\f'), '"\\\\f"');
} }
public function testMacrosAreDetected()
{
$this->assertFalse(c::stringHasMacro('$$vars$'));
$this->assertFalse(c::stringHasMacro('$$'));
$this->assertTrue(c::stringHasMacro('$vars$$'));
$this->assertTrue(c::stringHasMacro('$multiple$$vars.nested.name$$vars$ is here'));
$this->assertTrue(c::stringHasMacro('some $vars.nested.name$ is here'));
$this->assertTrue(c::stringHasMacro('some $vars.nested.name$$vars.even.more$'));
$this->assertTrue(c::stringHasMacro('$vars.nested.name$$a$$$$not$'));
$this->assertTrue(c::stringHasMacro('MSSQL$$$config$'));
$this->assertTrue(c::stringHasMacro('MSSQL$$$config$', 'config'));
$this->assertTrue(c::stringHasMacro('MSSQL$$$nix$ and $config$', 'config'));
$this->assertFalse(c::stringHasMacro('MSSQL$$$nix$config$ and $$', 'config'));
$this->assertFalse(c::stringHasMacro('MSSQL$$$nix$ and $$config$', 'config'));
$this->assertFalse(c::stringHasMacro('MSSQL$$$config$', 'conf'));
}
public function testRenderStringWithVariables() public function testRenderStringWithVariables()
{ {
$this->assertEquals(c::renderStringWithVariables('Before $var$'), '"Before " + var'); $this->assertEquals('"Before " + var', c::renderStringWithVariables('Before $var$'));
$this->assertEquals(c::renderStringWithVariables('$var$ After'), 'var + " After"'); $this->assertEquals(c::renderStringWithVariables('$var$ After'), 'var + " After"');
$this->assertEquals(c::renderStringWithVariables('$var$'), 'var'); $this->assertEquals(c::renderStringWithVariables('$var$'), 'var');
$this->assertEquals(c::renderStringWithVariables('$$var$$'), '"$$var$$"'); $this->assertEquals(c::renderStringWithVariables('$$var$$'), '"$$var$$"');
$this->assertEquals(c::renderStringWithVariables('Before $$var$$ After'), '"Before $$var$$ After"'); $this->assertEquals(c::renderStringWithVariables('Before $$var$$ After'), '"Before $$var$$ After"');
$this->assertEquals( $this->assertEquals(
c::renderStringWithVariables('Before $name$ $name$ After'), '"Before " + name1 + " " + name2 + " After"',
'"Before " + name + " " + name + " After"' c::renderStringWithVariables('Before $name1$ $name2$ After')
);
}
public function testRenderStringWithVariablesX()
{
$this->assertEquals(
'"Before " + var1 + " " + var2 + " After"',
c::renderStringWithVariables('Before $var1$ $var2$ After')
); );
$this->assertEquals( $this->assertEquals(
c::renderStringWithVariables('Before $var1$ $var2$ After'), 'host.vars.custom',
'"Before " + var1 + " " + var2 + " After"' c::renderStringWithVariables('$host.vars.custom$')
); );
$this->assertEquals(c::renderStringWithVariables('$host.vars.custom$'), 'host.vars.custom'); $this->assertEquals('"$var\"$"', c::renderStringWithVariables('$var"$'));
$this->assertEquals(c::renderStringWithVariables('$var"$'), '"$var\"$"');
$this->assertEquals( $this->assertEquals(
c::renderStringWithVariables('\tI am\rrendering\nproperly\fand I $support$ "multiple" $variables$\$'), '"\\\\tI am\\\\rrendering\\\\nproperly\\\\fand I " + support + " \"multiple\" " + variables + "\\\\$"',
'"\\\\tI am\\\\rrendering\\\\nproperly\\\\fand I " + support + " \"multiple\" " + variables + "\\\\$"' c::renderStringWithVariables('\tI am\rrendering\nproperly\fand I $support$ "multiple" $variables$\$')
); );
} }
} }