Skip to content

Commit b24a30c

Browse files
authored
yaml config env var spec conformance (#1486)
* add support for env: prefix in yaml config * align test to example in spec * do not recursively replace env vars - fix env substitution normalization so that it doesn't apply 2 replacement normalizers for string nodes
1 parent 7eb3abb commit b24a30c

File tree

2 files changed

+51
-3
lines changed

2 files changed

+51
-3
lines changed

src/Config/SDK/Configuration/Internal/EnvSubstitutionNormalization.php

+2-3
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ private function doApply(NodeDefinition $node): void
5050
default => FILTER_DEFAULT,
5151
};
5252
$node->beforeNormalization()->ifString()->then(fn (string $v) => $this->replaceEnvVariables($v, $filter))->end();
53-
}
54-
if ($node instanceof VariableNodeDefinition) {
53+
} elseif ($node instanceof VariableNodeDefinition) {
5554
$node->beforeNormalization()->always($this->replaceEnvVariablesRecursive(...))->end();
5655
}
5756

@@ -65,7 +64,7 @@ private function doApply(NodeDefinition $node): void
6564
private function replaceEnvVariables(string $value, int $filter = FILTER_DEFAULT): mixed
6665
{
6766
$replaced = preg_replace_callback(
68-
'/\$\{(?<ENV_NAME>[a-zA-Z_]\w*)(?::-(?<DEFAULT_VALUE>[^\n]*))?}/',
67+
'/\$\{(?:env:)?(?<ENV_NAME>[a-zA-Z_]\w*)(?::-(?<DEFAULT_VALUE>[^\n]*))?}/',
6968
fn (array $matches): string => $this->envReader->read($matches['ENV_NAME']) ?? $matches['DEFAULT_VALUE'] ?? '',
7069
$value,
7170
-1,

tests/Unit/Config/SDK/Configuration/ConfigurationFactoryTest.php

+49
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $buil
6464
$node
6565
->children()
6666
->scalarNode('string_key')->end()
67+
->scalarNode('env_string_key')->end()
6768
->scalarNode('other_string_key')->end()
6869
->scalarNode('another_string_key')->end()
6970
->scalarNode('string_key_with_quoted_hex_value')->end()
@@ -76,6 +77,7 @@ public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $buil
7677
->scalarNode('string_key_with_default')->end()
7778
->variableNode('undefined_key')->end()
7879
->variableNode('${STRING_VALUE}')->end()
80+
->scalarNode('recursive_key')->end()
7981
->end()
8082
;
8183

@@ -90,6 +92,8 @@ public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $buil
9092
'FLOAT_VALUE' => '1.1',
9193
'HEX_VALUE' => '0xdeadbeef',
9294
'INVALID_MAP_VALUE' => "value\nkey:value",
95+
'DO_NOT_REPLACE_ME' => 'Never use this value', // An unused environment variable
96+
'REPLACE_ME' => '${DO_NOT_REPLACE_ME}', // A valid replacement text, used verbatim, not replaced with "Never use this value"
9397
]),
9498
]),
9599
);
@@ -98,6 +102,7 @@ public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $buil
98102
$parsed = $factory->process([
99103
Yaml::parse(<<<'YAML'
100104
string_key: ${STRING_VALUE} # Valid reference to STRING_VALUE
105+
env_string_key: ${env:STRING_VALUE} # Valid reference to STRING_VALUE
101106
other_string_key: "${STRING_VALUE}" # Valid reference to STRING_VALUE inside double quotes
102107
another_string_key: "${BOOl_VALUE}" # Valid reference to BOOl_VALUE inside double quotes
103108
string_key_with_quoted_hex_value: "${HEX_VALUE}" # Valid reference to HEX_VALUE inside double quotes
@@ -110,12 +115,14 @@ public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $buil
110115
string_key_with_default: ${UNDEFINED_KEY:-fallback} # UNDEFINED_KEY is not defined but a default value is included
111116
undefined_key: ${UNDEFINED_KEY} # Invalid reference, UNDEFINED_KEY is not defined and is replaced with ""
112117
${STRING_VALUE}: value # Invalid reference, substitution is not valid in mapping keys and reference is ignored
118+
recursive_key: ${REPLACE_ME} # Valid reference to REPLACE_ME
113119
YAML),
114120
]);
115121

116122
$this->assertSame(
117123
Yaml::parse(<<<'YAML'
118124
string_key: value # Interpreted as type string, tag URI tag:yaml.org,2002:str
125+
env_string_key: value # Interpreted as type string, tag URI tag:yaml.org,2002:str
119126
other_string_key: "value" # Interpreted as type string, tag URI tag:yaml.org,2002:str
120127
another_string_key: "true" # Interpreted as type string, tag URI tag:yaml.org,2002:str
121128
string_key_with_quoted_hex_value: "0xdeadbeef" # Interpreted as type string, tag URI tag:yaml.org,2002:str
@@ -129,6 +136,7 @@ public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $buil
129136
# undefined_key removed as null is treated as unset
130137
undefined_key: # Interpreted as type null, tag URI tag:yaml.org,2002:null
131138
${STRING_VALUE}: value # Interpreted as type string, tag URI tag:yaml.org,2002:str
139+
recursive_key: ${DO_NOT_REPLACE_ME} # Interpreted as type string, tag URI tag:yaml.org,2002:str
132140
YAML),
133141
self::getPropertiesFromPlugin($parsed),
134142
);
@@ -152,6 +160,24 @@ public function test_env_substitution_string(): void
152160
$this->assertSame('example-service', self::getPropertiesFromPlugin($parsed)['resource']['attributes']['service.name']);
153161
}
154162

163+
#[BackupGlobals(true)]
164+
#[CoversNothing]
165+
public function test_env_substitution_with_env_prefix(): void
166+
{
167+
$_SERVER['OTEL_SERVICE_NAME'] = 'example-service';
168+
$parsed = self::factory()->process([[
169+
'file_format' => '0.1',
170+
'resource' => [
171+
'attributes' => [
172+
'service.name' => '${env:OTEL_SERVICE_NAME}',
173+
],
174+
],
175+
]]);
176+
177+
$this->assertInstanceOf(ComponentPlugin::class, $parsed);
178+
$this->assertSame('example-service', self::getPropertiesFromPlugin($parsed)['resource']['attributes']['service.name']);
179+
}
180+
155181
#[BackupGlobals(true)]
156182
public function test_env_substitution_non_string(): void
157183
{
@@ -167,6 +193,29 @@ public function test_env_substitution_non_string(): void
167193
$this->assertSame(2048, self::getPropertiesFromPlugin($parsed)['attribute_limits']['attribute_value_length_limit']);
168194
}
169195

196+
/**
197+
* It MUST NOT be possible to inject environment variable by environment variables.
198+
* For example, see references to DO_NOT_REPLACE_ME environment variable
199+
*/
200+
#[BackupGlobals(true)]
201+
#[CoversNothing]
202+
public function test_env_substitution_recursive_does_not_inject_environment_variables(): void
203+
{
204+
$_SERVER['DO_NOT_REPLACE_ME'] = 'Never use this value';
205+
$_SERVER['REPLACE_ME'] = '${DO_NOT_REPLACE_ME}';
206+
$parsed = self::factory()->process([[
207+
'file_format' => '0.1',
208+
'resource' => [
209+
'attributes' => [
210+
'service.name' => '${REPLACE_ME}',
211+
],
212+
],
213+
]]);
214+
215+
$this->assertInstanceOf(ComponentPlugin::class, $parsed);
216+
$this->assertSame('${DO_NOT_REPLACE_ME}', self::getPropertiesFromPlugin($parsed)['resource']['attributes']['service.name']);
217+
}
218+
170219
/**
171220
* If a property has a default value defined (i.e. is _not_ required) and is
172221
* missing or present but null, Create MUST ensure the SDK component is configured

0 commit comments

Comments
 (0)