@@ -11,7 +11,7 @@ import (
1111)
1212
1313// VariableRegex matches ${variable_name} patterns
14- var VariableRegex = regexp .MustCompile (`\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}` )
14+ var VariableRegex = regexp .MustCompile (`\$\{([a-zA-Z_][a-zA-Z0-9_- ]*)\}` )
1515
1616// SecretRegex matches ${secrets.secret_name} patterns
1717var SecretRegex = regexp .MustCompile (`\$\{secrets\.([^}]*)\}` )
@@ -33,6 +33,7 @@ type Resolver struct {
3333 tenantID model.GlobalID
3434 logger zerolog.Logger
3535 secretEnabled bool
36+ variableEnabled bool
3637}
3738
3839// NewResolver creates a new interpolation resolver
@@ -43,85 +44,94 @@ func NewResolver(variableProvider VariableProvider, secretProvider SecretProvide
4344 tenantID : tenantID ,
4445 logger : logger ,
4546 secretEnabled : secretEnabled ,
47+ variableEnabled : variableProvider != nil ,
4648 }
4749}
4850
49- // Resolve performs string interpolation, replacing both variables and secrets
51+ // Resolve performs string interpolation, replacing both variables and secrets in a single pass
5052func (r * Resolver ) Resolve (ctx context.Context , value string ) (string , error ) {
5153 if value == "" {
5254 return "" , nil
5355 }
5456
55- // First resolve secrets if enabled
56- if r .secretEnabled {
57- resolvedValue , err := r .resolveSecrets (ctx , value )
58- if err != nil {
59- return "" , err
60- }
61- value = resolvedValue
57+ // If secrets are disabled, just process variables in the entire string
58+ if ! r .secretEnabled {
59+ return r .processVariables (value ), nil
6260 }
6361
64- // Then resolve variables
65- if r .variableProvider != nil {
66- resolvedValue , err := r .resolveVariables (value )
67- if err != nil {
68- return "" , err
69- }
70- value = resolvedValue
62+ // Step 1: Find all secret matches with their positions
63+ type secretMatch struct {
64+ start , end int
65+ name string
66+ placeholder string
7167 }
7268
73- return value , nil
74- }
75-
76- // resolveSecrets resolves ${secrets.secret_name} patterns
77- func (r * Resolver ) resolveSecrets (ctx context.Context , value string ) (string , error ) {
78- matches := SecretRegex .FindAllStringSubmatch (value , - 1 )
79- if len (matches ) == 0 {
80- return value , nil
81- }
82-
83- result := value
69+ var secretMatches []secretMatch
70+ matches := SecretRegex .FindAllStringSubmatchIndex (value , - 1 )
8471 for _ , match := range matches {
85- if len (match ) < 2 {
72+ if len (match ) < 4 {
8673 continue
8774 }
8875
89- secretName := match [1 ]
90- placeholder := match [0 ] // ${secrets.secret_name}
76+ secretName := value [ match [2 ]: match [ 3 ] ]
77+ placeholder := value [ match [0 ]: match [ 1 ]]
9178
92- // Validate secret name follows Kubernetes DNS subdomain convention
79+ // Validate secret name follows Kubernetes DNS subdomain naming convention
9380 if ! isValidSecretName (secretName ) {
9481 return "" , fmt .Errorf ("invalid secret name '%s': must follow Kubernetes DNS subdomain naming convention" , secretName )
9582 }
9683
97- r .logger .Debug ().Str ("secretName" , secretName ).Int64 ("tenantId" , int64 (r .tenantID )).Msg ("resolving secret from GSM" )
84+ secretMatches = append (secretMatches , secretMatch {
85+ start : match [0 ],
86+ end : match [1 ],
87+ name : secretName ,
88+ placeholder : placeholder ,
89+ })
90+ }
9891
99- secretValue , err := r .secretProvider .GetSecretValue (ctx , r .tenantID , secretName )
92+ // Step 2: Split string into parts and process each part
93+ var result strings.Builder
94+ lastPos := 0
95+
96+ for _ , secretMatch := range secretMatches {
97+ // Process the part before this secret (non-secret part)
98+ nonSecretPart := value [lastPos :secretMatch .start ]
99+ if nonSecretPart != "" {
100+ processedPart := r .processVariables (nonSecretPart )
101+ result .WriteString (processedPart )
102+ }
103+
104+ // Process the secret part
105+ r .logger .Debug ().Str ("secretName" , secretMatch .name ).Int64 ("tenantId" , int64 (r .tenantID )).Msg ("resolving secret from GSM" )
106+
107+ secretValue , err := r .secretProvider .GetSecretValue (ctx , r .tenantID , secretMatch .name )
100108 if err != nil {
101- return "" , fmt .Errorf ("failed to get secret '%s' from GSM: %w" , secretName , err )
109+ return "" , fmt .Errorf ("failed to get secret '%s' from GSM: %w" , secretMatch . name , err )
102110 }
103111
104- // Replace the placeholder with the actual secret value
105- result = strings . ReplaceAll ( result , placeholder , secretValue )
112+ result . WriteString ( secretValue )
113+ lastPos = secretMatch . end
106114 }
107115
108- return result , nil
109- }
110-
111- // resolveVariables resolves ${variable_name} patterns
112- func (r * Resolver ) resolveVariables (value string ) (string , error ) {
113- // If no variable provider is set, return the value as-is
114- if r .variableProvider == nil {
115- return value , nil
116+ // Process the remaining part after the last secret (non-secret part)
117+ remainingPart := value [lastPos :]
118+ if remainingPart != "" {
119+ processedPart := r .processVariables (remainingPart )
120+ result .WriteString (processedPart )
116121 }
117122
118- matches := VariableRegex .FindAllStringSubmatch (value , - 1 )
119- if len (matches ) == 0 {
120- return value , nil
123+ return result .String (), nil
124+ }
125+
126+ // processVariables resolves ${variable_name} patterns in a string
127+ func (r * Resolver ) processVariables (value string ) string {
128+ if ! r .variableEnabled {
129+ return value
121130 }
122131
123132 result := value
124- for _ , match := range matches {
133+ variableMatches := VariableRegex .FindAllStringSubmatch (result , - 1 )
134+ for _ , match := range variableMatches {
125135 if len (match ) < 2 {
126136 continue
127137 }
@@ -131,14 +141,16 @@ func (r *Resolver) resolveVariables(value string) (string, error) {
131141
132142 varValue , err := r .variableProvider .GetVariable (varName )
133143 if err != nil {
134- return "" , fmt .Errorf ("failed to get variable '%s': %w" , varName , err )
144+ // If variable is not found, leave the placeholder as-is
145+ // This allows for backward compatibility and flexible configuration
146+ continue
135147 }
136148
137149 // Replace the placeholder with the actual variable value
138150 result = strings .ReplaceAll (result , placeholder , varValue )
139151 }
140152
141- return result , nil
153+ return result
142154}
143155
144156// isValidSecretName validates that a secret name follows Kubernetes DNS subdomain naming convention.
0 commit comments