Skip to content

Commit 9a80f00

Browse files
committed
feat: add core secret infrastructure and string interpolation system
This PR adds the foundational components for secret store support: - Core secret infrastructure in internal/secrets/tenant.go with: - Enhanced SecretProvider interface with GetSecretValue method - CapabilityAwareSecretProvider for probe capability awareness - TenantSecrets implementation with caching support - Integration with Grafana Secret Manager (GSM) API - String interpolation system in internal/prober/interpolation/ with: - Variable and secret resolution (${variable} and ${secrets.secret_name}) - Resolver interface for both variables and secrets - Comprehensive test coverage - Dependencies: - github.com/grafana/gsm-api-go-client for GSM integration - github.com/patrickmn/go-cache for secret caching This is the first part of the secret store support feature. Subsequent PRs will integrate this infrastructure into the HTTP prober and other components.
1 parent 82a9512 commit 9a80f00

File tree

6 files changed

+967
-43
lines changed

6 files changed

+967
-43
lines changed

go.mod

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,28 @@ require (
4949
cel.dev/expr v0.24.0 // indirect
5050
github.com/andybalholm/brotli v1.1.1 // indirect
5151
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
52+
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
5253
github.com/beorn7/perks v1.0.1 // indirect
5354
github.com/buger/goterm v1.0.4 // indirect
5455
github.com/cespare/xxhash/v2 v2.3.0 // indirect
5556
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
5657
github.com/dennwc/varint v1.0.0 // indirect
5758
github.com/golang/protobuf v1.5.4 // indirect
5859
github.com/google/cel-go v0.25.0 // indirect
60+
github.com/grafana/gsm-api-go-client v0.0.0-20250408225536-13991b80a507 // indirect
5961
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
6062
github.com/kylelemons/godebug v1.1.0 // indirect
61-
github.com/mattn/go-colorable v0.1.13 // indirect
63+
github.com/mattn/go-colorable v0.1.14 // indirect
6264
github.com/mattn/go-isatty v0.0.20 // indirect
6365
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
66+
github.com/oapi-codegen/runtime v1.1.1 // indirect
67+
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
6468
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
6569
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
6670
github.com/prometheus/procfs v0.15.1 // indirect
71+
github.com/sirupsen/logrus v1.9.3 // indirect
6772
github.com/stoewer/go-strcase v1.2.0 // indirect
73+
go.k6.io/k6 v0.58.0 // indirect
6874
go.uber.org/atomic v1.11.0 // indirect
6975
golang.org/x/mod v0.27.0 // indirect
7076
golang.org/x/oauth2 v0.30.0 // indirect

go.sum

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,22 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJ
1616
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
1717
github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk=
1818
github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
19+
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
1920
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
2021
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
2122
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
2223
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
2324
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
2425
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
26+
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
27+
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
2528
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
2629
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
2730
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
2831
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
2932
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
3033
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
34+
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
3135
github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
3236
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
3337
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
@@ -80,6 +84,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU
8084
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
8185
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
8286
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
87+
github.com/grafana/gsm-api-go-client v0.0.0-20250408225536-13991b80a507 h1:A7MiNeoZgGM/Xd4V7+/cLEnXPz34JXsS/hZ8TK8hn8E=
88+
github.com/grafana/gsm-api-go-client v0.0.0-20250408225536-13991b80a507/go.mod h1:lUsYIxuMBnLafl8SvNHge+CnNpWRjMZw19+iCA+JVA4=
8389
github.com/grafana/loki/pkg/push v0.0.0-20241004191050-c2f38e18c6b8 h1:mMfKxRrvuJ8EqI6SMmlORN3BUqZfIAc55ARPhIwBXQ4=
8490
github.com/grafana/loki/pkg/push v0.0.0-20241004191050-c2f38e18c6b8/go.mod h1:lJEF/Wh5MYlmBem6tOYAFObkLsuikfrEf8Iy9AdMPiQ=
8591
github.com/grafana/mtr v0.1.1-0.20221107202107-a9806fdda166 h1:COSDtVDArtLKK9p+mkUlPXCfWFslQFVVuQos39vxQrU=
@@ -92,6 +98,7 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
9298
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
9399
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
94100
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
101+
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
95102
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
96103
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
97104
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -103,8 +110,9 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
103110
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
104111
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
105112
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
106-
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
107113
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
114+
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
115+
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
108116
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
109117
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
110118
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -120,9 +128,13 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
120128
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
121129
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
122130
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
131+
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
132+
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
123133
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
124134
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
125135
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
136+
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
137+
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
126138
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
127139
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
128140
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -150,22 +162,27 @@ github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG
150162
github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAoaohfjqMU=
151163
github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
152164
github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
153-
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
154-
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
165+
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
166+
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
155167
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
156168
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
157169
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
170+
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
171+
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
158172
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
159173
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
160174
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
161175
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
176+
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
162177
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
163178
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
164179
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
165180
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
166181
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
167182
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
183+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
168184
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
185+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
169186
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
170187
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
171188
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@@ -176,6 +193,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
176193
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
177194
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
178195
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
196+
go.k6.io/k6 v0.58.0 h1:jgIQwYiyd1UQJ4XWii3Ch9OPthvvvwoNAz9WboV4kds=
197+
go.k6.io/k6 v0.58.0/go.mod h1:+HSPGg6h2MnNz/C3MDbKRNzKhkM0kSwQlQxcY5vxzMc=
179198
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
180199
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
181200
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
@@ -223,6 +242,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
223242
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
224243
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
225244
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
245+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
226246
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
227247
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
228248
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package interpolation
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"regexp"
7+
"strings"
8+
9+
"github.com/grafana/synthetic-monitoring-agent/internal/model"
10+
"github.com/rs/zerolog"
11+
)
12+
13+
// VariableRegex matches ${variable_name} patterns
14+
var VariableRegex = regexp.MustCompile(`\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}`)
15+
16+
// SecretRegex matches ${secrets.secret_name} patterns
17+
var SecretRegex = regexp.MustCompile(`\$\{secrets\.([^}]*)\}`)
18+
19+
// VariableProvider defines the interface for resolving variables
20+
type VariableProvider interface {
21+
GetVariable(name string) (string, error)
22+
}
23+
24+
// SecretProvider defines the interface for resolving secrets
25+
type SecretProvider interface {
26+
GetSecretValue(ctx context.Context, tenantID model.GlobalID, secretKey string) (string, error)
27+
}
28+
29+
// Resolver handles string interpolation for both variables and secrets
30+
type Resolver struct {
31+
variableProvider VariableProvider
32+
secretProvider SecretProvider
33+
tenantID model.GlobalID
34+
logger zerolog.Logger
35+
secretEnabled bool
36+
}
37+
38+
// NewResolver creates a new interpolation resolver
39+
func NewResolver(variableProvider VariableProvider, secretProvider SecretProvider, tenantID model.GlobalID, logger zerolog.Logger, secretEnabled bool) *Resolver {
40+
return &Resolver{
41+
variableProvider: variableProvider,
42+
secretProvider: secretProvider,
43+
tenantID: tenantID,
44+
logger: logger,
45+
secretEnabled: secretEnabled,
46+
}
47+
}
48+
49+
// Resolve performs string interpolation, replacing both variables and secrets
50+
func (r *Resolver) Resolve(ctx context.Context, value string) (string, error) {
51+
if value == "" {
52+
return "", nil
53+
}
54+
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
62+
}
63+
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
71+
}
72+
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
84+
for _, match := range matches {
85+
if len(match) < 2 {
86+
continue
87+
}
88+
89+
secretName := match[1]
90+
placeholder := match[0] // ${secrets.secret_name}
91+
92+
// Validate secret name follows Kubernetes DNS subdomain convention
93+
if !isValidSecretName(secretName) {
94+
return "", fmt.Errorf("invalid secret name '%s': must follow Kubernetes DNS subdomain naming convention", secretName)
95+
}
96+
97+
r.logger.Debug().Str("secretName", secretName).Int64("tenantId", int64(r.tenantID)).Msg("resolving secret from GSM")
98+
99+
secretValue, err := r.secretProvider.GetSecretValue(ctx, r.tenantID, secretName)
100+
if err != nil {
101+
return "", fmt.Errorf("failed to get secret '%s' from GSM: %w", secretName, err)
102+
}
103+
104+
// Replace the placeholder with the actual secret value
105+
result = strings.ReplaceAll(result, placeholder, secretValue)
106+
}
107+
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+
}
117+
118+
matches := VariableRegex.FindAllStringSubmatch(value, -1)
119+
if len(matches) == 0 {
120+
return value, nil
121+
}
122+
123+
result := value
124+
for _, match := range matches {
125+
if len(match) < 2 {
126+
continue
127+
}
128+
129+
varName := match[1]
130+
placeholder := match[0] // ${variable_name}
131+
132+
varValue, err := r.variableProvider.GetVariable(varName)
133+
if err != nil {
134+
return "", fmt.Errorf("failed to get variable '%s': %w", varName, err)
135+
}
136+
137+
// Replace the placeholder with the actual variable value
138+
result = strings.ReplaceAll(result, placeholder, varValue)
139+
}
140+
141+
return result, nil
142+
}
143+
144+
// isValidSecretName validates that a secret name follows Kubernetes DNS subdomain naming convention.
145+
// See: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
146+
func isValidSecretName(name string) bool {
147+
if len(name) == 0 || len(name) > 253 {
148+
return false
149+
}
150+
151+
// Must consist of lowercase alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character
152+
if !regexp.MustCompile(`^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$`).MatchString(name) {
153+
return false
154+
}
155+
156+
return true
157+
}
158+
159+
// ToJavaScript converts a string with variable interpolation to JavaScript code
160+
// This is used by multihttp to generate JavaScript that references variables
161+
func ToJavaScript(value string) string {
162+
if len(value) == 0 {
163+
return `''`
164+
}
165+
166+
var s strings.Builder
167+
buf := []byte(value)
168+
locs := VariableRegex.FindAllSubmatchIndex(buf, -1)
169+
170+
p := 0
171+
for _, loc := range locs {
172+
if len(loc) < 4 { // put the bounds checker at ease
173+
panic("unexpected result while building JavaScript")
174+
}
175+
176+
if s.Len() > 0 {
177+
s.WriteRune('+')
178+
}
179+
180+
if pre := buf[p:loc[0]]; len(pre) > 0 {
181+
s.WriteRune('\'')
182+
escapeJavaScript(&s, pre)
183+
s.WriteRune('\'')
184+
s.WriteRune('+')
185+
}
186+
187+
s.WriteString(`vars['`)
188+
// Because of the capture in the regular expression, the result
189+
// has two indices that represent the matched substring, and
190+
// two more indices that represent the capture group.
191+
s.Write(buf[loc[2]:loc[3]])
192+
s.WriteString(`']`)
193+
194+
p = loc[1]
195+
}
196+
197+
if len(buf[p:]) > 0 {
198+
if s.Len() > 0 {
199+
s.WriteRune('+')
200+
}
201+
202+
s.WriteRune('\'')
203+
escapeJavaScript(&s, buf[p:])
204+
s.WriteRune('\'')
205+
}
206+
207+
return s.String()
208+
}
209+
210+
// escapeJavaScript escapes a byte slice for use in JavaScript strings
211+
func escapeJavaScript(s *strings.Builder, buf []byte) {
212+
for _, b := range buf {
213+
switch b {
214+
case '\'':
215+
s.WriteString(`\'`)
216+
case '"':
217+
s.WriteString(`\"`)
218+
case '\\':
219+
s.WriteString(`\\`)
220+
case '\n':
221+
s.WriteString(`\n`)
222+
case '\r':
223+
s.WriteString(`\r`)
224+
case '\t':
225+
s.WriteString(`\t`)
226+
default:
227+
if b < 32 || b > 126 {
228+
// Escape non-printable characters
229+
fmt.Fprintf(s, `\x%02x`, b)
230+
} else {
231+
s.WriteByte(b)
232+
}
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)