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)
{
if ($renderExpressions) {
return c::renderStringWithVariables($this->getValue());
return c::renderStringWithVariables($this->getValue(), ['config']);
} else {
return c::renderString($this->getValue());
}

View File

@ -2,8 +2,7 @@
namespace Icinga\Module\Director\IcingaConfig;
use Icinga\Exception\IcingaException;
use Icinga\Exception\ProgrammingError;
use InvalidArgumentException;
class IcingaConfigHelper
{
@ -11,7 +10,7 @@ class IcingaConfigHelper
* Reserved words according to
* https://docs.icinga.com/icinga2/snapshot/doc/module/icinga2/chapter/language-reference#reserved-keywords
*/
protected static $reservedWords = array(
protected static $reservedWords = [
'object',
'template',
'include',
@ -39,7 +38,7 @@ class IcingaConfigHelper
'in',
'current_filename',
'current_line',
);
];
public static function renderKeyValue($key, $value, $prefix = ' ')
{
@ -69,7 +68,10 @@ class IcingaConfigHelper
} elseif ($value === 'n' || $value === false) {
return 'false';
} 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?
public static function renderString($string)
{
$special = array(
$special = [
'/\\\/',
'/"/',
'/\$/',
@ -106,10 +108,10 @@ class IcingaConfigHelper
'/\r/',
'/\n/',
// '/\b/', -> doesn't work
'/\f/'
);
'/\f/',
];
$replace = array(
$replace = [
'\\\\\\',
'\\"',
'\\$',
@ -118,7 +120,7 @@ class IcingaConfigHelper
'\\n',
// '\\b',
'\\f',
);
];
$string = preg_replace($special, $replace, $string);
@ -144,7 +146,10 @@ class IcingaConfigHelper
} elseif (is_string($value)) {
return static::renderString($value);
} 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
public static function renderArray($array)
{
$data = array();
$data = [];
foreach ($array as $entry) {
if ($entry instanceof IcingaConfigRenderer) {
$data[] = $entry;
@ -186,7 +191,7 @@ class IcingaConfigHelper
public static function renderDictionary($dictionary)
{
$vals = array();
$vals = [];
foreach ($dictionary as $key => $value) {
$vals[$key] = rtrim(
self::renderKeyValue(
@ -259,11 +264,12 @@ class IcingaConfigHelper
$value = 0;
foreach ($parts as $part) {
if (! preg_match('/^(\d+)([dhms]?)$/', $part, $m)) {
throw new ProgrammingError(
throw new InvalidArgumentException(sprintf(
'"%s" is not a valid time (duration) definition',
$interval
);
));
}
switch ($m[2]) {
case 'd':
$value += $m[1] * 86400;
@ -290,11 +296,11 @@ class IcingaConfigHelper
return '0s';
}
$steps = array(
$steps = [
'd' => 86400,
'h' => 3600,
'm' => 60,
);
];
foreach ($steps as $unit => $duration) {
if ($seconds % $duration === 0) {
@ -305,37 +311,89 @@ class IcingaConfigHelper
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(
'/(?<!\$)\$([\w\.]+)\$(?!\$)/',
'" + ${1} + "',
static::renderString($string)
);
return preg_match('/^[A-z_][A-z_\.\d]+$/', $name)
&& ! preg_match('/\.$/', $name);
}
// TODO: this is an exemption for special variables. It would
// never make any sense to evaluate them at parse time.
// Another issue remains: there might be other reasons
// for "late evaluation". But how to distinguish those
// use cases?
$string = preg_replace(
'/" \+ ((?:user|notification)\.[\w\.]+) \+ "/',
'\$${1}\$',
$string
);
public static function renderStringWithVariables($string, array $whiteList = null)
{
$len = strlen($string);
$start = false;
$parts = [];
// TODO: UTF8...
$offset = 0;
for ($i = 0; $i < $len; $i++) {
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) === '"" + ') {
$string = substr($string, 5);
}
if (substr($string, -5) === ' + ""') {
$string = substr($string, 0, -5);
$start = false;
}
}
}
}
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;
use Icinga\Exception\ProgrammingError;
use InvalidArgumentException;
class IcingaConfigRendered implements IcingaConfigRenderer
{
@ -11,7 +11,7 @@ class IcingaConfigRendered implements IcingaConfigRenderer
public function __construct($string)
{
if (! is_string($string)) {
throw new ProgrammingError('IcingaConfigRendered accepts only strings');
throw new InvalidArgumentException('IcingaConfigRendered accepts only strings');
}
$this->rendered = $string;

View File

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

View File

@ -19,7 +19,7 @@ class IcingaConfigHelperTest extends BaseTestCase
}
/**
* @expectedException \Icinga\Exception\ProgrammingError
* @expectedException \InvalidArgumentException
*/
public function testWhetherInvalidIntervalStringRaisesException()
{
@ -53,12 +53,12 @@ class IcingaConfigHelperTest extends BaseTestCase
public function testWhetherDictionaryRendersCorrectly()
{
$dict = (object) array(
$dict = (object) [
'key1' => 'bla',
'include' => 'reserved',
'spe cial' => 'value',
'0' => 'numeric',
);
];
$this->assertEquals(
c::renderDictionary($dict),
rtrim($this->loadRendered('dict1'))
@ -81,26 +81,50 @@ class IcingaConfigHelperTest extends BaseTestCase
$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()
{
$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$'), 'var');
$this->assertEquals(c::renderStringWithVariables('$$var$$'), '"$$var$$"');
$this->assertEquals(c::renderStringWithVariables('Before $$var$$ After'), '"Before $$var$$ After"');
$this->assertEquals(
c::renderStringWithVariables('Before $name$ $name$ After'),
'"Before " + name + " " + name + " After"'
'"Before " + name1 + " " + name2 + " After"',
c::renderStringWithVariables('Before $name1$ $name2$ After')
);
}
public function testRenderStringWithVariablesX()
{
$this->assertEquals(
'"Before " + var1 + " " + var2 + " After"',
c::renderStringWithVariables('Before $var1$ $var2$ After')
);
$this->assertEquals(
c::renderStringWithVariables('Before $var1$ $var2$ After'),
'"Before " + var1 + " " + var2 + " After"'
'host.vars.custom',
c::renderStringWithVariables('$host.vars.custom$')
);
$this->assertEquals(c::renderStringWithVariables('$host.vars.custom$'), 'host.vars.custom');
$this->assertEquals(c::renderStringWithVariables('$var"$'), '"$var\"$"');
$this->assertEquals('"$var\"$"', c::renderStringWithVariables('$var"$'));
$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$\$')
);
}
}