@@ -20,6 +20,7 @@ import (
2020 "errors"
2121 "fmt"
2222 "net/url"
23+ "os"
2324 "strings"
2425 "sync"
2526
@@ -29,6 +30,20 @@ import (
2930 "go.opentelemetry.io/collector/consumer/consumererror"
3031)
3132
33+ const (
34+ // expandPrefixChar is the char used to prefix strings that can be expanded,
35+ // either environment variables or config sources.
36+ expandPrefixChar = '$'
37+ // configSourceNameDelimChar is the char used to terminate the name of config source
38+ // when it is used to retrieve values to inject in the configuration
39+ configSourceNameDelimChar = ':'
40+ )
41+
42+ // private error types to help with testability
43+ type (
44+ errUnknownConfigSource struct { error }
45+ )
46+
3247// Manager is used to inject data from config sources into a configuration and also
3348// to monitor for updates on the items injected into the configuration. All methods
3449// of a Manager must be called only once and have a expected sequence:
@@ -44,13 +59,17 @@ import (
4459//
4560// param_to_be_retrieved: $<cfgSrcName>:<selector>[?<params_url_query_format>]
4661//
62+ // bracketed single-line:
63+ //
64+ // param_to_be_retrieved: ${<cfgSrcName>:<selector>[?<params_url_query_format>]}
65+ //
4766// and multi-line are supported:
4867//
4968// param_to_be_retrieved: |
5069// $<cfgSrcName>: <selector>
5170// [<params_multi_line_YAML>]
5271//
53- // The <cfgSrcName> is a name string used to indentify the config source instance to be used
72+ // The <cfgSrcName> is a name string used to identify the config source instance to be used
5473// to retrieve the value.
5574//
5675// The <selector> is the mandatory parameter required when retrieving data from a config source.
@@ -64,7 +83,7 @@ import (
6483// component:
6584// config_field: $file:/etc/secret.bin?binary=true
6685//
67- // For mult -line format <params_multi_line_YAML> uses syntax as a YAML inside YAML. Possible usage
86+ // For multi -line format <params_multi_line_YAML> uses syntax as a YAML inside YAML. Possible usage
6887// example in a YAML file:
6988//
7089// component:
@@ -91,6 +110,48 @@ import (
91110// # as a string if params doesn't specify that "binary" is true.
92111// text_from_file: $file:/etc/text.txt
93112//
113+ // Bracketed single-line should be used when concatenating a suffix to the value retrieved by
114+ // the config source. Example:
115+ //
116+ // component:
117+ // # Retrieves the value of the environment variable LOGS_DIR and appends /component.log to it.
118+ // log_file_fullname: ${env:LOGS_DIR}/component.log
119+ //
120+ // Environment variables are expanded before passed to the config source when used in the selector or
121+ // the optional parameters. Example:
122+ //
123+ // component:
124+ // # Retrieves the value from the file text.txt located on the path specified by the environment
125+ // # variable DATA_PATH. The name of the environment variable is the string after the delimiter
126+ // # until the first character different than '_' and non-alpha-numeric.
127+ // text_from_file: $file:$DATA_PATH/text.txt
128+ //
129+ // Since environment variables and config sources both use the '$', with or without brackets, as a prefix
130+ // for their expansion it is necessary to have a way to distinguish between them. For the non-bracketed
131+ // syntax the code will peek at the first character other than alpha-numeric and '_' after the '$'. If
132+ // that character is a ':' it will treat it as a config source and as environment variable otherwise.
133+ // For example:
134+ //
135+ // component:
136+ // field_0: $PATH:/etc/logs # Injects the data from a config sourced named "PATH" using the selector "/etc/logs".
137+ // field_1: $PATH/etc/logs # Expands the environment variable "PATH" and adds the suffix "/etc/logs" to it.
138+ //
139+ // So if you need to include an environment followed by ':' the bracketed syntax must be used instead:
140+ //
141+ // component:
142+ // field_0: ${PATH}:/etc/logs # Expands the environment variable "PATH" and adds the suffix ":/etc/logs" to it.
143+ //
144+ // For the bracketed syntax the presence of ':' inside the brackets indicates that code will treat the bracketed
145+ // contents as a config source. For example:
146+ //
147+ // component:
148+ // field_0: ${file:/var/secret.txt} # Injects the data from a config sourced named "file" using the selector "/var/secret.txt".
149+ // field_1: ${file}:/var/secret.txt # Expands the environment variable "file" and adds the suffix ":/var/secret.txt" to it.
150+ //
151+ // If the the character following the '$' is in the set {'*', '#', '$', '@', '!', '?', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
152+ // the code will consider it to be the name of an environment variable to expand, or config source if followed by ':'. Do not use any of these
153+ // characters as the first char on the name of a config source or an environment variable (even if allowed by the system) to avoid unexpected
154+ // results.
94155type Manager struct {
95156 // configSources is map from ConfigSource names (as defined in the configuration)
96157 // and the respective instances.
@@ -114,7 +175,7 @@ type Manager struct {
114175// NewManager creates a new instance of a Manager to be used to inject data from
115176// ConfigSource objects into a configuration and watch for updates on the injected
116177// data.
117- func NewManager (* config.Parser ) (* Manager , error ) {
178+ func NewManager (_ * config.Parser ) (* Manager , error ) {
118179 // TODO: Config sources should be extracted for the config itself, need Factories for that.
119180
120181 return & Manager {
@@ -236,7 +297,7 @@ func (m *Manager) retrieveEndAllSessions(ctx context.Context) []error {
236297func (m * Manager ) expandStringValues (ctx context.Context , value interface {}) (interface {}, error ) {
237298 switch v := value .(type ) {
238299 case string :
239- return m .expandConfigSources (ctx , v )
300+ return m .expandString (ctx , v )
240301 case []interface {}:
241302 nslice := make ([]interface {}, 0 , len (v ))
242303 for _ , vint := range v {
@@ -262,32 +323,16 @@ func (m *Manager) expandStringValues(ctx context.Context, value interface{}) (in
262323 }
263324}
264325
265- // expandConfigSources retrieve data from the specified config sources and injects them into
326+ // expandConfigSource retrieve data from the specified config source and injects them into
266327// the configuration. The Manager tracks sessions and watcher objects as needed.
267- func (m * Manager ) expandConfigSources (ctx context.Context , s string ) (interface {}, error ) {
268- // Provisional implementation: only strings prefixed with the first character '$'
269- // are checked for config sources.
270- //
271- // TODO: Handle concatenated with other strings (needs delimiter syntax);
272- //
273- if len (s ) == 0 || s [0 ] != '$' {
274- // TODO: handle escaped $.
275- return s , nil
276- }
277-
278- cfgSrcName , selector , params , err := parseCfgSrc (s [1 :])
328+ func (m * Manager ) expandConfigSource (ctx context.Context , cfgSrc ConfigSource , s string ) (interface {}, error ) {
329+ cfgSrcName , selector , params , err := parseCfgSrc (s )
279330 if err != nil {
280331 return nil , err
281332 }
282333
283334 session , ok := m .sessions [cfgSrcName ]
284335 if ! ok {
285- // The session for this config source was not created yet.
286- cfgSrc , ok := m .configSources [cfgSrcName ]
287- if ! ok {
288- return nil , fmt .Errorf ("config source %q not found" , cfgSrcName )
289- }
290-
291336 session , err = cfgSrc .NewSession (ctx )
292337 if err != nil {
293338 return nil , fmt .Errorf ("failed to create session for config source %q: %w" , cfgSrcName , err )
@@ -305,12 +350,129 @@ func (m *Manager) expandConfigSources(ctx context.Context, s string) (interface{
305350 return retrieved .Value (), nil
306351}
307352
353+ // expandString expands environment variables and config sources that are specified on the string.
354+ func (m * Manager ) expandString (ctx context.Context , s string ) (interface {}, error ) {
355+ // Code based on os.Expand function. All delimiters that are checked against are
356+ // ASCII so bytes are fine for this operation.
357+ var buf []byte
358+
359+ // Using i, j, and w variables to keep correspondence with os.Expand code.
360+ // i tracks the index in s from which a slice to be appended to buf should start.
361+ // j tracks the char being currently checked and also the end of the slice to be appended to buf.
362+ // w tracks the number of characters being consumed after a prefix identifying env vars or config sources.
363+ i := 0
364+ for j := 0 ; j < len (s ); j ++ {
365+ if s [j ] == expandPrefixChar && j + 1 < len (s ) {
366+ if buf == nil {
367+ // Assuming that the length of the string will double after expansion of env vars and config sources.
368+ buf = make ([]byte , 0 , 2 * len (s ))
369+ }
370+
371+ // Append everything consumed up to the prefix char (but not including the prefix char) to the result.
372+ buf = append (buf , s [i :j ]... )
373+
374+ var expandableContent , cfgSrcName string
375+ w := 0 // number of bytes consumed on this pass
376+
377+ switch {
378+ case s [j + 1 ] == expandPrefixChar :
379+ // Escaping the prefix so $$ becomes a single $ without attempting
380+ // to treat the string after it as a config source or env var.
381+ expandableContent = string (expandPrefixChar )
382+ w = 1 // consumed a single char
383+
384+ case s [j + 1 ] == '{' :
385+ // Bracketed usage, consume everything until first '}' exactly as os.Expand.
386+ expandableContent , w = getShellName (s [j + 1 :])
387+ expandableContent = strings .Trim (expandableContent , " " ) // Allow for some spaces.
388+ if len (expandableContent ) > 1 && strings .Contains (expandableContent , string (configSourceNameDelimChar )) {
389+ // Bracket expandableContent contains ':' treating it as a config source.
390+ cfgSrcName , _ = getShellName (expandableContent )
391+ }
392+
393+ default :
394+ // Non-bracketed usage, ie.: found the prefix char, it can be either a config
395+ // source or an environment variable.
396+ var name string
397+ name , w = getShellName (s [j + 1 :])
398+ expandableContent = name // Assume for now that it is an env var.
399+
400+ // Peek next char after name, if it is a config source name delimiter treat the remaining of the
401+ // string as a config source.
402+ if j + w + 1 < len (s ) && s [j + w + 1 ] == configSourceNameDelimChar {
403+ // This is a config source, since it is not delimited it will consume until end of the string.
404+ cfgSrcName = name
405+ expandableContent = s [j + 1 :]
406+ w = len (expandableContent ) // Set consumed bytes to the length of expandableContent
407+ }
408+ }
409+
410+ switch {
411+ case cfgSrcName == "" :
412+ // Not a config source, expand as os.ExpandEnv
413+ buf = osExpandEnv (buf , expandableContent , w )
414+
415+ default :
416+ // A config source, retrieve and apply results.
417+ retrieved , err := m .retrieveConfigSourceData (ctx , cfgSrcName , expandableContent )
418+ if err != nil {
419+ return nil , err
420+ }
421+
422+ consumedAll := j + w + 1 == len (s )
423+ if consumedAll && len (buf ) == 0 {
424+ // This is the only expandableContent on the string, config
425+ // source is free to return interface{}.
426+ return retrieved , nil
427+ }
428+
429+ // Either there was a prefix already or there are still
430+ // characters to be processed.
431+ buf = append (buf , fmt .Sprintf ("%v" , retrieved )... )
432+ }
433+
434+ j += w // move the index of the char being checked (j) by the number of characters consumed (w) on this iteration.
435+ i = j + 1 // update start index (i) of next slice of bytes to be copied.
436+ }
437+ }
438+
439+ if buf == nil {
440+ // No changes to original string, just return it.
441+ return s , nil
442+ }
443+
444+ // Return whatever was accumulated on the buffer plus the remaining of the original string.
445+ return string (buf ) + s [i :], nil
446+ }
447+
448+ func (m * Manager ) retrieveConfigSourceData (ctx context.Context , name , cfgSrcInvoke string ) (interface {}, error ) {
449+ cfgSrc , ok := m .configSources [name ]
450+ if ! ok {
451+ return nil , newErrUnknownConfigSource (name )
452+ }
453+
454+ // Expand any env vars on the selector and parameters. Nested config source usage
455+ // is not supported.
456+ cfgSrcInvoke = expandEnvVars (cfgSrcInvoke )
457+ retrieved , err := m .expandConfigSource (ctx , cfgSrc , cfgSrcInvoke )
458+ if err != nil {
459+ return nil , err
460+ }
461+
462+ return retrieved , nil
463+ }
464+
465+ func newErrUnknownConfigSource (cfgSrcName string ) error {
466+ return & errUnknownConfigSource {
467+ fmt .Errorf (`config source %q not found if this was intended to be an environment variable use "${%s}" instead"` , cfgSrcName , cfgSrcName ),
468+ }
469+ }
470+
308471// parseCfgSrc extracts the reference to a config source from a string value.
309472// The caller should check for error explicitly since it is possible for the
310473// other values to have been partially set.
311474func parseCfgSrc (s string ) (cfgSrcName , selector string , params interface {}, err error ) {
312- const cfgSrcDelim string = ":"
313- parts := strings .SplitN (s , cfgSrcDelim , 2 )
475+ parts := strings .SplitN (s , string (configSourceNameDelimChar ), 2 )
314476 if len (parts ) != 2 {
315477 err = fmt .Errorf ("invalid config source syntax at %q, it must have at least the config source name and a selector" , s )
316478 return
@@ -386,3 +548,82 @@ func parseParamsAsURLQuery(s string) (interface{}, error) {
386548 }
387549 return params , err
388550}
551+
552+ // expandEnvVars is used to expand environment variables with the same syntax used
553+ // by config.Parser.
554+ func expandEnvVars (s string ) string {
555+ return os .Expand (s , func (str string ) string {
556+ // This allows escaping environment variable substitution via $$, e.g.
557+ // - $FOO will be substituted with env var FOO
558+ // - $$FOO will be replaced with $FOO
559+ // - $$$FOO will be replaced with $ + substituted env var FOO
560+ if str == "$" {
561+ return "$"
562+ }
563+ return os .Getenv (str )
564+ })
565+ }
566+
567+ // osExpandEnv replicate the internal behavior of os.ExpandEnv when handling env
568+ // vars updating the buffer accordingly.
569+ func osExpandEnv (buf []byte , name string , w int ) []byte {
570+ switch {
571+ case name == "" && w > 0 :
572+ // Encountered invalid syntax; eat the
573+ // characters.
574+ case name == "" || name == "$" :
575+ // Valid syntax, but $ was not followed by a
576+ // name. Leave the dollar character untouched.
577+ buf = append (buf , expandPrefixChar )
578+ default :
579+ buf = append (buf , os .Getenv (name )... )
580+ }
581+
582+ return buf
583+ }
584+
585+ // Below are helper functions used by os.Expand, copied without changes from original sources (env.go).
586+
587+ // isShellSpecialVar reports whether the character identifies a special
588+ // shell variable such as $*.
589+ func isShellSpecialVar (c uint8 ) bool {
590+ switch c {
591+ case '*' , '#' , '$' , '@' , '!' , '?' , '-' , '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' :
592+ return true
593+ }
594+ return false
595+ }
596+
597+ // isAlphaNum reports whether the byte is an ASCII letter, number, or underscore
598+ func isAlphaNum (c uint8 ) bool {
599+ return c == '_' || '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z'
600+ }
601+
602+ // getShellName returns the name that begins the string and the number of bytes
603+ // consumed to extract it. If the name is enclosed in {}, it's part of a ${}
604+ // expansion and two more bytes are needed than the length of the name.
605+ func getShellName (s string ) (string , int ) {
606+ switch {
607+ case s [0 ] == '{' :
608+ if len (s ) > 2 && isShellSpecialVar (s [1 ]) && s [2 ] == '}' {
609+ return s [1 :2 ], 3
610+ }
611+ // Scan to closing brace
612+ for i := 1 ; i < len (s ); i ++ {
613+ if s [i ] == '}' {
614+ if i == 1 {
615+ return "" , 2 // Bad syntax; eat "${}"
616+ }
617+ return s [1 :i ], i + 1
618+ }
619+ }
620+ return "" , 1 // Bad syntax; eat "${"
621+ case isShellSpecialVar (s [0 ]):
622+ return s [0 :1 ], 1
623+ }
624+ // Scan alphanumerics.
625+ var i int
626+ for i = 0 ; i < len (s ) && isAlphaNum (s [i ]); i ++ {
627+ }
628+ return s [:i ], i
629+ }
0 commit comments