feat: webhooker 1.0 MVP — entity rename, core engine, delivery, management UI #16
@@ -34,7 +34,6 @@ RUN set -eux; \
|
||||
|
||||
# Copy go module files and download dependencies
|
||||
COPY go.mod go.sum ./
|
||||
COPY pkg/config/go.mod pkg/config/go.sum ./pkg/config/
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
|
||||
22
README.md
22
README.md
@@ -50,17 +50,12 @@ make hooks # Install git pre-commit hook that runs make check
|
||||
|
||||
### Configuration
|
||||
|
||||
webhooker uses a YAML configuration file with environment-specific
|
||||
overrides, loaded via the `pkg/config` library (Viper-based). The
|
||||
environment is selected by setting `WEBHOOKER_ENVIRONMENT` to `dev` or
|
||||
`prod` (default: `dev`).
|
||||
All configuration is via environment variables. For local development,
|
||||
you can place variables in a `.env` file in the project root (loaded
|
||||
automatically via `godotenv/autoload`).
|
||||
|
||||
Configuration is resolved in this order (highest priority first):
|
||||
|
||||
1. Environment variables
|
||||
2. `.env` file (loaded via `godotenv/autoload`)
|
||||
3. Config file values for the active environment
|
||||
4. Config file defaults
|
||||
The environment is selected by setting `WEBHOOKER_ENVIRONMENT` to `dev`
|
||||
or `prod` (default: `dev`).
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ----------------------- | ----------------------------------- | -------- |
|
||||
@@ -692,7 +687,7 @@ webhooker/
|
||||
│ └── main.go # Entry point: sets globals, wires fx
|
||||
├── internal/
|
||||
│ ├── config/
|
||||
│ │ └── config.go # Configuration loading via pkg/config
|
||||
│ │ └── config.go # Configuration loading from environment variables
|
||||
│ ├── database/
|
||||
│ │ ├── base_model.go # BaseModel with UUID primary keys
|
||||
│ │ ├── database.go # GORM connection, migrations, admin seed
|
||||
@@ -733,14 +728,11 @@ webhooker/
|
||||
│ │ └── routes.go # All route definitions
|
||||
│ └── session/
|
||||
│ └── session.go # Cookie-based session management
|
||||
├── pkg/config/ # Reusable multi-environment config library
|
||||
├── static/
|
||||
│ ├── static.go # //go:embed directive
|
||||
│ ├── css/style.css # Custom stylesheet (system font stack, card effects, layout)
|
||||
│ └── js/app.js # Client-side JavaScript (minimal bootstrap)
|
||||
├── templates/ # Go HTML templates (base, index, login, etc.)
|
||||
├── configs/
|
||||
│ └── config.yaml.example # Example configuration file
|
||||
├── Dockerfile # Multi-stage: build + check, then Alpine runtime
|
||||
├── Makefile # fmt, lint, test, check, build, docker targets
|
||||
├── go.mod / go.sum
|
||||
@@ -753,7 +745,7 @@ Components are wired via Uber fx in this order:
|
||||
|
||||
1. `globals.New` — Build-time variables (appname, version, arch)
|
||||
2. `logger.New` — Structured logging (slog with TTY detection)
|
||||
3. `config.New` — Configuration loading (pkg/config + environment)
|
||||
3. `config.New` — Configuration loading (environment variables)
|
||||
4. `database.New` — Main SQLite connection, config migrations, admin
|
||||
user seed
|
||||
5. `database.NewWebhookDBManager` — Per-webhook event database
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
environments:
|
||||
dev:
|
||||
config:
|
||||
port: 8080
|
||||
debug: true
|
||||
maintenanceMode: false
|
||||
developmentMode: true
|
||||
environment: dev
|
||||
# Database URL for local development
|
||||
dburl: postgres://webhooker:webhooker@localhost:5432/webhooker_dev?sslmode=disable
|
||||
# Basic auth for metrics endpoint in dev
|
||||
metricsUsername: admin
|
||||
metricsPassword: admin
|
||||
# Dev admin credentials for testing
|
||||
devAdminUsername: devadmin
|
||||
devAdminPassword: devpassword
|
||||
secrets:
|
||||
# Sentry DSN - usually not needed in dev
|
||||
sentryDSN: ""
|
||||
|
||||
prod:
|
||||
config:
|
||||
port: $ENV:PORT
|
||||
debug: $ENV:DEBUG
|
||||
maintenanceMode: $ENV:MAINTENANCE_MODE
|
||||
developmentMode: false
|
||||
environment: prod
|
||||
dburl: $ENV:DBURL
|
||||
metricsUsername: $ENV:METRICS_USERNAME
|
||||
metricsPassword: $ENV:METRICS_PASSWORD
|
||||
# Dev admin credentials should not be set in production
|
||||
devAdminUsername: ""
|
||||
devAdminPassword: ""
|
||||
secrets:
|
||||
sentryDSN: $ENV:SENTRY_DSN
|
||||
|
||||
configDefaults:
|
||||
# These defaults apply to all environments unless overridden
|
||||
port: 8080
|
||||
debug: false
|
||||
maintenanceMode: false
|
||||
developmentMode: false
|
||||
environment: dev
|
||||
metricsUsername: ""
|
||||
metricsPassword: ""
|
||||
devAdminUsername: ""
|
||||
devAdminPassword: ""
|
||||
25
go.mod
25
go.mod
@@ -14,35 +14,22 @@ require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/slok/go-http-metrics v0.11.0
|
||||
github.com/spf13/afero v1.14.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.uber.org/fx v1.20.1
|
||||
golang.org/x/crypto v0.38.0
|
||||
gorm.io/driver/sqlite v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
modernc.org/sqlite v1.28.0
|
||||
sneak.berlin/go/webhooker/pkg/config v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.23.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.5 // indirect
|
||||
cloud.google.com/go/secretmanager v1.11.4 // indirect
|
||||
github.com/aws/aws-sdk-go v1.50.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -53,25 +40,15 @@ require (
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/dig v1.17.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go.uber.org/zap v1.23.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/oauth2 v0.15.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/api v0.153.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
@@ -84,5 +61,3 @@ require (
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
)
|
||||
|
||||
replace sneak.berlin/go/webhooker/pkg/config => ./pkg/config
|
||||
|
||||
138
go.sum
138
go.sum
@@ -1,28 +1,11 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
|
||||
cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
|
||||
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
|
||||
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
|
||||
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
|
||||
cloud.google.com/go/secretmanager v1.11.4 h1:krnX9qpG2kR2fJ+u+uNyNo+ACVhplIAS4Pu7u+4gd+k=
|
||||
cloud.google.com/go/secretmanager v1.11.4/go.mod h1:wreJlbS9Zdq21lMzWmJ0XhWW2ZxgPeahsqeV/vZoJ3w=
|
||||
github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8 h1:nMpu1t4amK3vJWBibQ5X/Nv0aXL+b69TQf2uK5PH7Go=
|
||||
github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/aws/aws-sdk-go v1.50.0 h1:HBtrLeO+QyDKnc3t1+5DR1RxodOHCGr8ZcrHudpv7jI=
|
||||
github.com/aws/aws-sdk-go v1.50.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -30,10 +13,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI=
|
||||
github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||
@@ -42,30 +21,7 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -73,15 +29,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
@@ -90,10 +39,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
@@ -117,7 +62,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
@@ -130,21 +74,12 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/slok/go-http-metrics v0.11.0 h1:ABJUpekCZSkQT1wQrFvS4kGbhea/w6ndFJaWJeh3zL0=
|
||||
github.com/slok/go-http-metrics v0.11.0/go.mod h1:ZGKeYG1ET6TEJpQx18BqAJAvxw9jBAZXCHU7bWQqqAc=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
|
||||
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI=
|
||||
@@ -157,105 +92,32 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
|
||||
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4=
|
||||
google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
|
||||
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/webhooker/internal/globals"
|
||||
"sneak.berlin/go/webhooker/internal/logger"
|
||||
pkgconfig "sneak.berlin/go/webhooker/pkg/config"
|
||||
|
||||
// Populates the environment from a ./.env file automatically for
|
||||
// development configuration. Kept in one place only (here).
|
||||
@@ -56,38 +55,30 @@ func (c *Config) IsProd() bool {
|
||||
return c.Environment == EnvironmentProd
|
||||
}
|
||||
|
||||
// envString returns the env var value if set, otherwise falls back to pkgconfig.
|
||||
func envString(envKey, configKey string) string {
|
||||
if v := os.Getenv(envKey); v != "" {
|
||||
return v
|
||||
}
|
||||
return pkgconfig.GetString(configKey)
|
||||
// envString returns the value of the named environment variable, or
|
||||
// an empty string if not set.
|
||||
func envString(key string) string {
|
||||
return os.Getenv(key)
|
||||
}
|
||||
|
||||
// envSecretString returns the env var value if set, otherwise falls back to pkgconfig secrets.
|
||||
func envSecretString(envKey, configKey string) string {
|
||||
if v := os.Getenv(envKey); v != "" {
|
||||
return v
|
||||
}
|
||||
return pkgconfig.GetSecretString(configKey)
|
||||
}
|
||||
|
||||
// envBool returns the env var value parsed as bool, otherwise falls back to pkgconfig.
|
||||
func envBool(envKey, configKey string) bool {
|
||||
if v := os.Getenv(envKey); v != "" {
|
||||
// envBool returns the value of the named environment variable parsed as a
|
||||
// boolean. Returns defaultValue if not set.
|
||||
func envBool(key string, defaultValue bool) bool {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return strings.EqualFold(v, "true") || v == "1"
|
||||
}
|
||||
return pkgconfig.GetBool(configKey)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// envInt returns the env var value parsed as int, otherwise falls back to pkgconfig.
|
||||
func envInt(envKey, configKey string, defaultValue ...int) int {
|
||||
if v := os.Getenv(envKey); v != "" {
|
||||
// envInt returns the value of the named environment variable parsed as an
|
||||
// integer. Returns defaultValue if not set or unparseable.
|
||||
func envInt(key string, defaultValue int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return pkgconfig.GetInt(configKey, defaultValue...)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// nolint:revive // lc parameter is required by fx even if unused
|
||||
@@ -106,21 +97,18 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
||||
EnvironmentDev, EnvironmentProd, environment)
|
||||
}
|
||||
|
||||
// Set the environment in the config package (for fallback resolution)
|
||||
pkgconfig.SetEnvironment(environment)
|
||||
|
||||
// Load configuration values — env vars take precedence over config.yaml
|
||||
// Load configuration values from environment variables
|
||||
s := &Config{
|
||||
DBURL: envString("DBURL", "dburl"),
|
||||
DataDir: envString("DATA_DIR", "dataDir"),
|
||||
Debug: envBool("DEBUG", "debug"),
|
||||
MaintenanceMode: envBool("MAINTENANCE_MODE", "maintenanceMode"),
|
||||
DevelopmentMode: envBool("DEVELOPMENT_MODE", "developmentMode"),
|
||||
DBURL: envString("DBURL"),
|
||||
DataDir: envString("DATA_DIR"),
|
||||
Debug: envBool("DEBUG", false),
|
||||
MaintenanceMode: envBool("MAINTENANCE_MODE", false),
|
||||
DevelopmentMode: envBool("DEVELOPMENT_MODE", false),
|
||||
Environment: environment,
|
||||
MetricsUsername: envString("METRICS_USERNAME", "metricsUsername"),
|
||||
MetricsPassword: envString("METRICS_PASSWORD", "metricsPassword"),
|
||||
Port: envInt("PORT", "port", 8080),
|
||||
SentryDSN: envSecretString("SENTRY_DSN", "sentryDSN"),
|
||||
MetricsUsername: envString("METRICS_USERNAME"),
|
||||
MetricsPassword: envString("METRICS_PASSWORD"),
|
||||
Port: envInt("PORT", 8080),
|
||||
SentryDSN: envString("SENTRY_DSN"),
|
||||
log: log,
|
||||
params: ¶ms,
|
||||
}
|
||||
|
||||
@@ -4,58 +4,14 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxtest"
|
||||
"sneak.berlin/go/webhooker/internal/globals"
|
||||
"sneak.berlin/go/webhooker/internal/logger"
|
||||
pkgconfig "sneak.berlin/go/webhooker/pkg/config"
|
||||
)
|
||||
|
||||
// createTestConfig creates a test configuration file in memory
|
||||
func createTestConfig(fs afero.Fs) error {
|
||||
configYAML := `
|
||||
environments:
|
||||
dev:
|
||||
config:
|
||||
port: 8080
|
||||
debug: true
|
||||
maintenanceMode: false
|
||||
developmentMode: true
|
||||
environment: dev
|
||||
dburl: postgres://test:test@localhost:5432/test_dev?sslmode=disable
|
||||
metricsUsername: testuser
|
||||
metricsPassword: testpass
|
||||
secrets:
|
||||
sentryDSN: ""
|
||||
|
||||
prod:
|
||||
config:
|
||||
port: $ENV:PORT
|
||||
debug: $ENV:DEBUG
|
||||
maintenanceMode: $ENV:MAINTENANCE_MODE
|
||||
developmentMode: false
|
||||
environment: prod
|
||||
dburl: $ENV:DBURL
|
||||
metricsUsername: $ENV:METRICS_USERNAME
|
||||
metricsPassword: $ENV:METRICS_PASSWORD
|
||||
secrets:
|
||||
sentryDSN: $ENV:SENTRY_DSN
|
||||
|
||||
configDefaults:
|
||||
port: 8080
|
||||
debug: false
|
||||
maintenanceMode: false
|
||||
developmentMode: false
|
||||
environment: dev
|
||||
metricsUsername: ""
|
||||
metricsPassword: ""
|
||||
`
|
||||
return afero.WriteFile(fs, "config.yaml", []byte(configYAML), 0644)
|
||||
}
|
||||
|
||||
func TestEnvironmentConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -68,6 +24,7 @@ func TestEnvironmentConfig(t *testing.T) {
|
||||
{
|
||||
name: "default is dev",
|
||||
envValue: "",
|
||||
envVars: map[string]string{"DBURL": "file::memory:?cache=shared"},
|
||||
expectError: false,
|
||||
isDev: true,
|
||||
isProd: false,
|
||||
@@ -75,6 +32,7 @@ func TestEnvironmentConfig(t *testing.T) {
|
||||
{
|
||||
name: "explicit dev",
|
||||
envValue: "dev",
|
||||
envVars: map[string]string{"DBURL": "file::memory:?cache=shared"},
|
||||
expectError: false,
|
||||
isDev: true,
|
||||
isProd: false,
|
||||
@@ -92,21 +50,19 @@ func TestEnvironmentConfig(t *testing.T) {
|
||||
{
|
||||
name: "invalid environment",
|
||||
envValue: "staging",
|
||||
envVars: map[string]string{"DBURL": "file::memory:?cache=shared"},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create in-memory filesystem with test config
|
||||
fs := afero.NewMemMapFs()
|
||||
require.NoError(t, createTestConfig(fs))
|
||||
pkgconfig.SetFs(fs)
|
||||
|
||||
// Set environment variable if specified
|
||||
if tt.envValue != "" {
|
||||
os.Setenv("WEBHOOKER_ENVIRONMENT", tt.envValue)
|
||||
defer os.Unsetenv("WEBHOOKER_ENVIRONMENT")
|
||||
} else {
|
||||
os.Unsetenv("WEBHOOKER_ENVIRONMENT")
|
||||
}
|
||||
|
||||
// Set additional environment variables
|
||||
|
||||
@@ -2,38 +2,19 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"go.uber.org/fx/fxtest"
|
||||
"sneak.berlin/go/webhooker/internal/config"
|
||||
"sneak.berlin/go/webhooker/internal/globals"
|
||||
"sneak.berlin/go/webhooker/internal/logger"
|
||||
pkgconfig "sneak.berlin/go/webhooker/pkg/config"
|
||||
)
|
||||
|
||||
func TestDatabaseConnection(t *testing.T) {
|
||||
// Set up in-memory config so the test does not depend on config.yaml on disk
|
||||
fs := afero.NewMemMapFs()
|
||||
testConfigYAML := `
|
||||
environments:
|
||||
dev:
|
||||
config:
|
||||
port: 8080
|
||||
debug: false
|
||||
maintenanceMode: false
|
||||
developmentMode: true
|
||||
environment: dev
|
||||
dburl: "file::memory:?cache=shared"
|
||||
secrets:
|
||||
sentryDSN: ""
|
||||
configDefaults:
|
||||
port: 8080
|
||||
`
|
||||
if err := afero.WriteFile(fs, "config.yaml", []byte(testConfigYAML), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
pkgconfig.SetFs(fs)
|
||||
// Set DBURL env var for config loading
|
||||
os.Setenv("DBURL", "file::memory:?cache=shared")
|
||||
defer os.Unsetenv("DBURL")
|
||||
|
||||
// Set up test dependencies
|
||||
lc := fxtest.NewLifecycle(t)
|
||||
|
||||
@@ -7,32 +7,20 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/fx/fxtest"
|
||||
"sneak.berlin/go/webhooker/internal/config"
|
||||
"sneak.berlin/go/webhooker/internal/globals"
|
||||
"sneak.berlin/go/webhooker/internal/logger"
|
||||
pkgconfig "sneak.berlin/go/webhooker/pkg/config"
|
||||
)
|
||||
|
||||
func setupTestWebhookDBManager(t *testing.T) (*WebhookDBManager, *fxtest.Lifecycle) {
|
||||
t.Helper()
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
testConfigYAML := `
|
||||
environments:
|
||||
dev:
|
||||
config:
|
||||
port: 8080
|
||||
debug: false
|
||||
dburl: "file::memory:?cache=shared"
|
||||
configDefaults:
|
||||
port: 8080
|
||||
`
|
||||
require.NoError(t, afero.WriteFile(fs, "config.yaml", []byte(testConfigYAML), 0644))
|
||||
pkgconfig.SetFs(fs)
|
||||
// Set DBURL env var for config loading
|
||||
os.Setenv("DBURL", "file::memory:?cache=shared")
|
||||
t.Cleanup(func() { os.Unsetenv("DBURL") })
|
||||
|
||||
lc := fxtest.NewLifecycle(t)
|
||||
|
||||
@@ -52,7 +40,6 @@ configDefaults:
|
||||
DBURL: "file::memory:?cache=shared",
|
||||
DataDir: dataDir,
|
||||
}
|
||||
_ = cfg
|
||||
|
||||
mgr, err := NewWebhookDBManager(lc, WebhookDBManagerParams{
|
||||
Config: cfg,
|
||||
|
||||
1
pkg/config/.gitignore
vendored
1
pkg/config/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
# Configuration Module (Go)
|
||||
|
||||
A simple, clean, and generic configuration management system that supports multiple environments and automatic value resolution. This module is completely standalone and can be used in any Go project.
|
||||
|
||||
## Features
|
||||
|
||||
- **Simple API**: Just `config.Get()` and `config.GetSecret()`
|
||||
- **Type-safe helpers**: `config.GetString()`, `config.GetInt()`, `config.GetBool()`
|
||||
- **Environment Support**: Separate configs for different environments (dev/prod/staging/etc)
|
||||
- **Value Resolution**: Automatic resolution of special values:
|
||||
- `$ENV:VARIABLE` - Read from environment variable
|
||||
- `$GSM:secret-name` - Read from Google Secret Manager
|
||||
- `$ASM:secret-name` - Read from AWS Secrets Manager
|
||||
- `$FILE:/path/to/file` - Read from file contents
|
||||
- **Hierarchical Defaults**: Environment-specific values override defaults
|
||||
- **YAML-based**: Easy to read and edit configuration files
|
||||
- **Thread-safe**: Safe for concurrent use
|
||||
- **Testable**: Uses afero filesystem abstraction for easy testing
|
||||
- **Minimal Dependencies**: Only requires YAML parser and cloud SDKs (optional)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get git.eeqj.de/sneak/webhooker/pkg/config
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.eeqj.de/sneak/webhooker/pkg/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Set the environment explicitly
|
||||
config.SetEnvironment("prod")
|
||||
|
||||
// Get configuration values
|
||||
baseURL := config.GetString("baseURL")
|
||||
apiTimeout := config.GetInt("timeout", 30)
|
||||
debugMode := config.GetBool("debugMode", false)
|
||||
|
||||
// Get secret values
|
||||
apiKey := config.GetSecretString("api_key")
|
||||
dbPassword := config.GetSecretString("db_password", "default")
|
||||
|
||||
// Get all values (for debugging)
|
||||
allConfig := config.GetAllConfig()
|
||||
allSecrets := config.GetAllSecrets()
|
||||
|
||||
// Reload configuration from file
|
||||
if err := config.Reload(); err != nil {
|
||||
fmt.Printf("Failed to reload config: %v\n", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration File Structure
|
||||
|
||||
Create a `config.yaml` file in your project root:
|
||||
|
||||
```yaml
|
||||
environments:
|
||||
dev:
|
||||
config:
|
||||
baseURL: https://dev.example.com
|
||||
debugMode: true
|
||||
timeout: 30
|
||||
secrets:
|
||||
api_key: dev-key-12345
|
||||
db_password: $ENV:DEV_DB_PASSWORD
|
||||
|
||||
prod:
|
||||
config:
|
||||
baseURL: https://prod.example.com
|
||||
debugMode: false
|
||||
timeout: 10
|
||||
GCPProject: my-project-123
|
||||
AWSRegion: us-west-2
|
||||
secrets:
|
||||
api_key: $GSM:prod-api-key
|
||||
db_password: $ASM:prod/db/password
|
||||
|
||||
configDefaults:
|
||||
app_name: my-app
|
||||
timeout: 30
|
||||
log_level: INFO
|
||||
port: 8080
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Environment Selection**: Call `config.SetEnvironment("prod")` to select which environment to use
|
||||
|
||||
2. **Value Lookup**: When you call `config.Get("key")`:
|
||||
- First checks `environments.<env>.config.key`
|
||||
- Falls back to `configDefaults.key`
|
||||
- Returns the default value if not found
|
||||
|
||||
3. **Secret Lookup**: When you call `config.GetSecret("key")`:
|
||||
- Looks in `environments.<env>.secrets.key`
|
||||
- Returns the default value if not found
|
||||
|
||||
4. **Value Resolution**: If a value starts with a special prefix:
|
||||
- `$ENV:` - Reads from environment variable
|
||||
- `$GSM:` - Fetches from Google Secret Manager (requires GCPProject to be set in config)
|
||||
- `$ASM:` - Fetches from AWS Secrets Manager (uses AWSRegion from config or defaults to us-east-1)
|
||||
- `$FILE:` - Reads from file (supports `~` expansion)
|
||||
|
||||
## Type-Safe Access
|
||||
|
||||
The module provides type-safe helper functions:
|
||||
|
||||
```go
|
||||
// String values
|
||||
baseURL := config.GetString("baseURL", "http://localhost")
|
||||
|
||||
// Integer values
|
||||
port := config.GetInt("port", 8080)
|
||||
|
||||
// Boolean values
|
||||
debug := config.GetBool("debug", false)
|
||||
|
||||
// Secret string values
|
||||
apiKey := config.GetSecretString("api_key", "default-key")
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
For local development, you can:
|
||||
|
||||
1. Use environment variables:
|
||||
```yaml
|
||||
secrets:
|
||||
api_key: $ENV:LOCAL_API_KEY
|
||||
```
|
||||
|
||||
2. Use local files:
|
||||
```yaml
|
||||
secrets:
|
||||
api_key: $FILE:~/.secrets/api-key.txt
|
||||
```
|
||||
|
||||
3. Create a `config.local.yaml` (gitignored) with literal values for testing
|
||||
|
||||
## Cloud Provider Support
|
||||
|
||||
### Google Secret Manager
|
||||
|
||||
To use GSM resolution (`$GSM:` prefix):
|
||||
1. Set `GCPProject` in your config
|
||||
2. Ensure proper authentication (e.g., `GOOGLE_APPLICATION_CREDENTIALS` environment variable)
|
||||
3. The module will automatically initialize the GSM client when needed
|
||||
|
||||
### AWS Secrets Manager
|
||||
|
||||
To use ASM resolution (`$ASM:` prefix):
|
||||
1. Optionally set `AWSRegion` in your config (defaults to us-east-1)
|
||||
2. Ensure proper authentication (e.g., AWS credentials in environment or IAM role)
|
||||
3. The module will automatically initialize the ASM client when needed
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Loading from a Specific File
|
||||
|
||||
```go
|
||||
// Load configuration from a specific file
|
||||
if err := config.LoadFile("/path/to/config.yaml"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Checking Configuration Values
|
||||
|
||||
```go
|
||||
// Get all configuration for current environment
|
||||
allConfig := config.GetAllConfig()
|
||||
for key, value := range allConfig {
|
||||
fmt.Printf("%s: %v\n", key, value)
|
||||
}
|
||||
|
||||
// Get all secrets (be careful with logging!)
|
||||
allSecrets := config.GetAllSecrets()
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The module uses the [afero](https://github.com/spf13/afero) filesystem abstraction, making it easy to test without real files:
|
||||
|
||||
```go
|
||||
package myapp_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/spf13/afero"
|
||||
"git.eeqj.de/sneak/webhooker/pkg/config"
|
||||
)
|
||||
|
||||
func TestMyApp(t *testing.T) {
|
||||
// Create an in-memory filesystem for testing
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
// Write a test config file
|
||||
testConfig := `
|
||||
environments:
|
||||
test:
|
||||
config:
|
||||
apiURL: http://test.example.com
|
||||
secrets:
|
||||
apiKey: test-key-123
|
||||
`
|
||||
afero.WriteFile(fs, "config.yaml", []byte(testConfig), 0644)
|
||||
|
||||
// Use the test filesystem
|
||||
config.SetFs(fs)
|
||||
config.SetEnvironment("test")
|
||||
|
||||
// Now your tests use the in-memory config
|
||||
if url := config.GetString("apiURL"); url != "http://test.example.com" {
|
||||
t.Errorf("Expected test URL, got %s", url)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unit Testing with Isolated Config
|
||||
|
||||
For unit tests, you can create isolated configuration managers:
|
||||
|
||||
```go
|
||||
func TestMyComponent(t *testing.T) {
|
||||
// Create a test-specific manager
|
||||
manager := config.NewManager()
|
||||
|
||||
// Use in-memory filesystem
|
||||
fs := afero.NewMemMapFs()
|
||||
afero.WriteFile(fs, "config.yaml", []byte(testConfig), 0644)
|
||||
manager.SetFs(fs)
|
||||
|
||||
// Test with isolated configuration
|
||||
manager.SetEnvironment("test")
|
||||
value := manager.Get("someKey", "default")
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If a config file is not found when using the default loader, an error is returned
|
||||
- If a key is not found, the default value is returned
|
||||
- If a special value cannot be resolved (e.g., env var not set, file not found), `nil` is returned
|
||||
- Cloud provider errors are logged but return `nil` to allow graceful degradation
|
||||
|
||||
## Thread Safety
|
||||
|
||||
All operations are thread-safe. The module uses read-write mutexes to ensure safe concurrent access to configuration data.
|
||||
|
||||
## Example Integration
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"git.eeqj.de/sneak/webhooker/pkg/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Read environment from your app-specific env var
|
||||
environment := os.Getenv("APP_ENV")
|
||||
if environment == "" {
|
||||
environment = "dev"
|
||||
}
|
||||
|
||||
config.SetEnvironment(environment)
|
||||
|
||||
// Now use configuration throughout your app
|
||||
databaseURL := config.GetString("database_url")
|
||||
apiKey := config.GetSecretString("api_key")
|
||||
|
||||
log.Printf("Running in %s environment", environment)
|
||||
log.Printf("Database URL: %s", databaseURL)
|
||||
}
|
||||
```
|
||||
|
||||
## Migration from Python Version
|
||||
|
||||
The Go version maintains API compatibility with the Python version where possible:
|
||||
|
||||
| Python | Go |
|
||||
|--------|-----|
|
||||
| `config.get('key')` | `config.Get("key")` or `config.GetString("key")` |
|
||||
| `config.getSecret('key')` | `config.GetSecret("key")` or `config.GetSecretString("key")` |
|
||||
| `config.set_environment('prod')` | `config.SetEnvironment("prod")` |
|
||||
| `config.reload()` | `config.Reload()` |
|
||||
| `config.get_all_config()` | `config.GetAllConfig()` |
|
||||
| `config.get_all_secrets()` | `config.GetAllSecrets()` |
|
||||
|
||||
## License
|
||||
|
||||
This module is designed to be standalone and can be extracted into its own repository with your preferred license.
|
||||
@@ -1,180 +0,0 @@
|
||||
// Package config provides a simple, clean, and generic configuration management system
|
||||
// that supports multiple environments and automatic value resolution.
|
||||
//
|
||||
// Features:
|
||||
// - Simple API: Just config.Get() and config.GetSecret()
|
||||
// - Environment Support: Separate configs for different environments (dev/prod/staging/etc)
|
||||
// - Value Resolution: Automatic resolution of special values:
|
||||
// - $ENV:VARIABLE - Read from environment variable
|
||||
// - $GSM:secret-name - Read from Google Secret Manager
|
||||
// - $ASM:secret-name - Read from AWS Secrets Manager
|
||||
// - $FILE:/path/to/file - Read from file contents
|
||||
// - Hierarchical Defaults: Environment-specific values override defaults
|
||||
// - YAML-based: Easy to read and edit configuration files
|
||||
// - Zero Dependencies: Only depends on yaml and cloud provider SDKs (optional)
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// import "sneak.berlin/go/webhooker/pkg/config"
|
||||
//
|
||||
// // Set the environment explicitly
|
||||
// config.SetEnvironment("prod")
|
||||
//
|
||||
// // Get configuration values
|
||||
// baseURL := config.Get("baseURL")
|
||||
// apiTimeout := config.GetInt("timeout", 30)
|
||||
//
|
||||
// // Get secret values
|
||||
// apiKey := config.GetSecret("api_key")
|
||||
// dbPassword := config.GetSecret("db_password", "default")
|
||||
package config
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Global configuration manager instance
|
||||
var (
|
||||
globalManager *Manager
|
||||
mu sync.Mutex // Protect global manager updates
|
||||
)
|
||||
|
||||
// getManager returns the global configuration manager, creating it if necessary
|
||||
func getManager() *Manager {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if globalManager == nil {
|
||||
globalManager = NewManager()
|
||||
}
|
||||
return globalManager
|
||||
}
|
||||
|
||||
// SetEnvironment sets the active environment.
|
||||
func SetEnvironment(environment string) {
|
||||
getManager().SetEnvironment(environment)
|
||||
}
|
||||
|
||||
// SetFs sets the filesystem to use for all file operations.
|
||||
// This is primarily useful for testing with an in-memory filesystem.
|
||||
func SetFs(fs afero.Fs) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// Create a new manager with the specified filesystem
|
||||
newManager := NewManager()
|
||||
newManager.SetFs(fs)
|
||||
|
||||
// Replace the global manager
|
||||
globalManager = newManager
|
||||
}
|
||||
|
||||
// Get retrieves a configuration value.
|
||||
//
|
||||
// This looks for values in the following order:
|
||||
// 1. Environment-specific config (environments.<env>.config.<key>)
|
||||
// 2. Config defaults (configDefaults.<key>)
|
||||
//
|
||||
// Values are resolved if they contain special prefixes:
|
||||
// - $ENV:VARIABLE_NAME - reads from environment variable
|
||||
// - $GSM:secret-name - reads from Google Secret Manager
|
||||
// - $ASM:secret-name - reads from AWS Secrets Manager
|
||||
// - $FILE:/path/to/file - reads from file
|
||||
func Get(key string, defaultValue ...interface{}) interface{} {
|
||||
var def interface{}
|
||||
if len(defaultValue) > 0 {
|
||||
def = defaultValue[0]
|
||||
}
|
||||
return getManager().Get(key, def)
|
||||
}
|
||||
|
||||
// GetString retrieves a configuration value as a string.
|
||||
func GetString(key string, defaultValue ...string) string {
|
||||
var def string
|
||||
if len(defaultValue) > 0 {
|
||||
def = defaultValue[0]
|
||||
}
|
||||
val := Get(key, def)
|
||||
if s, ok := val.(string); ok {
|
||||
return s
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// GetInt retrieves a configuration value as an integer.
|
||||
func GetInt(key string, defaultValue ...int) int {
|
||||
var def int
|
||||
if len(defaultValue) > 0 {
|
||||
def = defaultValue[0]
|
||||
}
|
||||
val := Get(key, def)
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
return v
|
||||
case int64:
|
||||
return int(v)
|
||||
case float64:
|
||||
return int(v)
|
||||
default:
|
||||
return def
|
||||
}
|
||||
}
|
||||
|
||||
// GetBool retrieves a configuration value as a boolean.
|
||||
func GetBool(key string, defaultValue ...bool) bool {
|
||||
var def bool
|
||||
if len(defaultValue) > 0 {
|
||||
def = defaultValue[0]
|
||||
}
|
||||
val := Get(key, def)
|
||||
if b, ok := val.(bool); ok {
|
||||
return b
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// GetSecret retrieves a secret value.
|
||||
//
|
||||
// This looks for secrets defined in environments.<env>.secrets.<key>
|
||||
func GetSecret(key string, defaultValue ...interface{}) interface{} {
|
||||
var def interface{}
|
||||
if len(defaultValue) > 0 {
|
||||
def = defaultValue[0]
|
||||
}
|
||||
return getManager().GetSecret(key, def)
|
||||
}
|
||||
|
||||
// GetSecretString retrieves a secret value as a string.
|
||||
func GetSecretString(key string, defaultValue ...string) string {
|
||||
var def string
|
||||
if len(defaultValue) > 0 {
|
||||
def = defaultValue[0]
|
||||
}
|
||||
val := GetSecret(key, def)
|
||||
if s, ok := val.(string); ok {
|
||||
return s
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// Reload reloads the configuration from file.
|
||||
func Reload() error {
|
||||
return getManager().Reload()
|
||||
}
|
||||
|
||||
// GetAllConfig returns all configuration values for the current environment.
|
||||
func GetAllConfig() map[string]interface{} {
|
||||
return getManager().GetAllConfig()
|
||||
}
|
||||
|
||||
// GetAllSecrets returns all secrets for the current environment.
|
||||
func GetAllSecrets() map[string]interface{} {
|
||||
return getManager().GetAllSecrets()
|
||||
}
|
||||
|
||||
// LoadFile loads configuration from a specific file.
|
||||
func LoadFile(configFile string) error {
|
||||
return getManager().LoadFile(configFile)
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
manager := NewManager()
|
||||
if manager == nil {
|
||||
t.Fatal("NewManager returned nil")
|
||||
}
|
||||
if manager.config == nil {
|
||||
t.Error("Manager config map is nil")
|
||||
}
|
||||
if manager.loader == nil {
|
||||
t.Error("Manager loader is nil")
|
||||
}
|
||||
if manager.resolvedCache == nil {
|
||||
t.Error("Manager resolvedCache is nil")
|
||||
}
|
||||
if manager.fs == nil {
|
||||
t.Error("Manager fs is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoader_FindConfigFile(t *testing.T) {
|
||||
// Create an in-memory filesystem for testing
|
||||
fs := afero.NewMemMapFs()
|
||||
loader := NewLoader(fs)
|
||||
|
||||
// Create a config file in the filesystem
|
||||
configContent := `
|
||||
environments:
|
||||
test:
|
||||
config:
|
||||
testKey: testValue
|
||||
secrets:
|
||||
testSecret: secretValue
|
||||
configDefaults:
|
||||
defaultKey: defaultValue
|
||||
`
|
||||
// Create the file in the current directory
|
||||
if err := afero.WriteFile(fs, "config.yaml", []byte(configContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Test finding the config file
|
||||
foundPath, err := loader.FindConfigFile("config.yaml")
|
||||
if err != nil {
|
||||
t.Errorf("FindConfigFile failed: %v", err)
|
||||
}
|
||||
|
||||
// In memory fs, the path should be exactly what we created
|
||||
if foundPath != "config.yaml" {
|
||||
t.Errorf("Expected config.yaml, got %s", foundPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoader_LoadYAML(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
loader := NewLoader(fs)
|
||||
|
||||
// Create a test config file
|
||||
testConfig := `
|
||||
environments:
|
||||
test:
|
||||
config:
|
||||
testKey: testValue
|
||||
configDefaults:
|
||||
defaultKey: defaultValue
|
||||
`
|
||||
if err := afero.WriteFile(fs, "test-config.yaml", []byte(testConfig), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Load the YAML
|
||||
config, err := loader.LoadYAML("test-config.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadYAML failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the structure
|
||||
envs, ok := config["environments"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("environments not found or wrong type")
|
||||
}
|
||||
|
||||
testEnv, ok := envs["test"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("test environment not found")
|
||||
}
|
||||
|
||||
testConfig2, ok := testEnv["config"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("test config not found")
|
||||
}
|
||||
|
||||
if testConfig2["testKey"] != "testValue" {
|
||||
t.Errorf("Expected testKey=testValue, got %v", testConfig2["testKey"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolver_ResolveEnv(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
resolver := NewResolver("", "", fs)
|
||||
|
||||
// Set a test environment variable
|
||||
os.Setenv("TEST_CONFIG_VAR", "test-value")
|
||||
defer os.Unsetenv("TEST_CONFIG_VAR")
|
||||
|
||||
// Test resolving environment variable
|
||||
result := resolver.Resolve("$ENV:TEST_CONFIG_VAR")
|
||||
if result != "test-value" {
|
||||
t.Errorf("Expected 'test-value', got %v", result)
|
||||
}
|
||||
|
||||
// Test non-existent env var
|
||||
result = resolver.Resolve("$ENV:NON_EXISTENT_VAR")
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for non-existent env var, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolver_ResolveFile(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
resolver := NewResolver("", "", fs)
|
||||
|
||||
// Create a test file
|
||||
secretContent := "my-secret-value"
|
||||
if err := afero.WriteFile(fs, "/test-secret.txt", []byte(secretContent+"\n"), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
// Test resolving file
|
||||
result := resolver.Resolve("$FILE:/test-secret.txt")
|
||||
if result != secretContent {
|
||||
t.Errorf("Expected '%s', got %v", secretContent, result)
|
||||
}
|
||||
|
||||
// Test non-existent file
|
||||
result = resolver.Resolve("$FILE:/non/existent/file")
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for non-existent file, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_GetAndSet(t *testing.T) {
|
||||
// Create an in-memory filesystem
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
// Create a test config file
|
||||
testConfig := `
|
||||
environments:
|
||||
dev:
|
||||
config:
|
||||
apiURL: http://dev.example.com
|
||||
timeout: 30
|
||||
debug: true
|
||||
secrets:
|
||||
apiKey: dev-key-123
|
||||
prod:
|
||||
config:
|
||||
apiURL: https://prod.example.com
|
||||
timeout: 10
|
||||
debug: false
|
||||
secrets:
|
||||
apiKey: $ENV:PROD_API_KEY
|
||||
configDefaults:
|
||||
appName: TestApp
|
||||
timeout: 20
|
||||
port: 8080
|
||||
`
|
||||
if err := afero.WriteFile(fs, "config.yaml", []byte(testConfig), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Create manager and set the filesystem
|
||||
manager := NewManager()
|
||||
manager.SetFs(fs)
|
||||
|
||||
// Load config should find the file automatically
|
||||
manager.SetEnvironment("dev")
|
||||
|
||||
// Test getting config values
|
||||
if v := manager.Get("apiURL", ""); v != "http://dev.example.com" {
|
||||
t.Errorf("Expected dev apiURL, got %v", v)
|
||||
}
|
||||
|
||||
if v := manager.Get("timeout", 0); v != 30 {
|
||||
t.Errorf("Expected timeout=30, got %v", v)
|
||||
}
|
||||
|
||||
if v := manager.Get("debug", false); v != true {
|
||||
t.Errorf("Expected debug=true, got %v", v)
|
||||
}
|
||||
|
||||
// Test default values
|
||||
if v := manager.Get("appName", ""); v != "TestApp" {
|
||||
t.Errorf("Expected appName from defaults, got %v", v)
|
||||
}
|
||||
|
||||
// Test getting secrets
|
||||
if v := manager.GetSecret("apiKey", ""); v != "dev-key-123" {
|
||||
t.Errorf("Expected dev apiKey, got %v", v)
|
||||
}
|
||||
|
||||
// Switch to prod environment
|
||||
manager.SetEnvironment("prod")
|
||||
|
||||
if v := manager.Get("apiURL", ""); v != "https://prod.example.com" {
|
||||
t.Errorf("Expected prod apiURL, got %v", v)
|
||||
}
|
||||
|
||||
// Test environment variable resolution in secrets
|
||||
os.Setenv("PROD_API_KEY", "prod-key-456")
|
||||
defer os.Unsetenv("PROD_API_KEY")
|
||||
|
||||
if v := manager.GetSecret("apiKey", ""); v != "prod-key-456" {
|
||||
t.Errorf("Expected resolved env var for apiKey, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalAPI(t *testing.T) {
|
||||
// Create an in-memory filesystem
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
// Create a test config file
|
||||
testConfig := `
|
||||
environments:
|
||||
test:
|
||||
config:
|
||||
stringVal: hello
|
||||
intVal: 42
|
||||
boolVal: true
|
||||
secrets:
|
||||
secret1: test-secret
|
||||
configDefaults:
|
||||
defaultString: world
|
||||
`
|
||||
if err := afero.WriteFile(fs, "config.yaml", []byte(testConfig), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Use the global API with the test filesystem
|
||||
SetFs(fs)
|
||||
SetEnvironment("test")
|
||||
|
||||
// Test type-safe getters
|
||||
if v := GetString("stringVal"); v != "hello" {
|
||||
t.Errorf("Expected 'hello', got %v", v)
|
||||
}
|
||||
|
||||
if v := GetInt("intVal"); v != 42 {
|
||||
t.Errorf("Expected 42, got %v", v)
|
||||
}
|
||||
|
||||
if v := GetBool("boolVal"); v != true {
|
||||
t.Errorf("Expected true, got %v", v)
|
||||
}
|
||||
|
||||
if v := GetSecretString("secret1"); v != "test-secret" {
|
||||
t.Errorf("Expected 'test-secret', got %v", v)
|
||||
}
|
||||
|
||||
// Test defaults
|
||||
if v := GetString("defaultString"); v != "world" {
|
||||
t.Errorf("Expected 'world', got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_SetFs(t *testing.T) {
|
||||
// Create manager with default OS filesystem
|
||||
manager := NewManager()
|
||||
|
||||
// Create an in-memory filesystem
|
||||
memFs := afero.NewMemMapFs()
|
||||
|
||||
// Write a config file to the memory fs
|
||||
testConfig := `
|
||||
environments:
|
||||
test:
|
||||
config:
|
||||
testKey: fromMemory
|
||||
configDefaults:
|
||||
defaultKey: memoryDefault
|
||||
`
|
||||
if err := afero.WriteFile(memFs, "config.yaml", []byte(testConfig), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Set the filesystem
|
||||
manager.SetFs(memFs)
|
||||
manager.SetEnvironment("test")
|
||||
|
||||
// Test that it reads from the memory filesystem
|
||||
if v := manager.Get("testKey", ""); v != "fromMemory" {
|
||||
t.Errorf("Expected 'fromMemory', got %v", v)
|
||||
}
|
||||
|
||||
if v := manager.Get("defaultKey", ""); v != "memoryDefault" {
|
||||
t.Errorf("Expected 'memoryDefault', got %v", v)
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"sneak.berlin/go/webhooker/pkg/config"
|
||||
)
|
||||
|
||||
// ExampleSetFs demonstrates how to use an in-memory filesystem for testing
|
||||
func ExampleSetFs() {
|
||||
// Create an in-memory filesystem
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
// Create a test configuration file
|
||||
configYAML := `
|
||||
environments:
|
||||
test:
|
||||
config:
|
||||
baseURL: https://test.example.com
|
||||
debugMode: true
|
||||
secrets:
|
||||
apiKey: test-key-12345
|
||||
production:
|
||||
config:
|
||||
baseURL: https://api.example.com
|
||||
debugMode: false
|
||||
configDefaults:
|
||||
appName: Test Application
|
||||
timeout: 30
|
||||
`
|
||||
|
||||
// Write the config to the in-memory filesystem
|
||||
if err := afero.WriteFile(fs, "config.yaml", []byte(configYAML), 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Use the in-memory filesystem
|
||||
config.SetFs(fs)
|
||||
config.SetEnvironment("test")
|
||||
|
||||
// Now all config operations use the in-memory filesystem
|
||||
fmt.Printf("Base URL: %s\n", config.GetString("baseURL"))
|
||||
fmt.Printf("Debug Mode: %v\n", config.GetBool("debugMode"))
|
||||
fmt.Printf("App Name: %s\n", config.GetString("appName"))
|
||||
|
||||
// Output:
|
||||
// Base URL: https://test.example.com
|
||||
// Debug Mode: true
|
||||
// App Name: Test Application
|
||||
}
|
||||
|
||||
// TestWithAferoFilesystem shows how to test with different filesystem implementations
|
||||
func TestWithAferoFilesystem(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFs func() afero.Fs
|
||||
environment string
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "in-memory filesystem",
|
||||
setupFs: func() afero.Fs {
|
||||
fs := afero.NewMemMapFs()
|
||||
config := `
|
||||
environments:
|
||||
dev:
|
||||
config:
|
||||
apiURL: http://localhost:8080
|
||||
`
|
||||
afero.WriteFile(fs, "config.yaml", []byte(config), 0644)
|
||||
return fs
|
||||
},
|
||||
environment: "dev",
|
||||
key: "apiURL",
|
||||
expected: "http://localhost:8080",
|
||||
},
|
||||
{
|
||||
name: "readonly filesystem",
|
||||
setupFs: func() afero.Fs {
|
||||
memFs := afero.NewMemMapFs()
|
||||
config := `
|
||||
environments:
|
||||
staging:
|
||||
config:
|
||||
apiURL: https://staging.example.com
|
||||
`
|
||||
afero.WriteFile(memFs, "config.yaml", []byte(config), 0644)
|
||||
// Wrap in a read-only filesystem
|
||||
return afero.NewReadOnlyFs(memFs)
|
||||
},
|
||||
environment: "staging",
|
||||
key: "apiURL",
|
||||
expected: "https://staging.example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a new manager for each test to ensure isolation
|
||||
manager := config.NewManager()
|
||||
manager.SetFs(tt.setupFs())
|
||||
manager.SetEnvironment(tt.environment)
|
||||
|
||||
result := manager.Get(tt.key, "")
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %s, got %v", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileResolution shows how $FILE: resolution works with afero
|
||||
func TestFileResolution(t *testing.T) {
|
||||
// Create an in-memory filesystem
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
// Create a secret file
|
||||
secretContent := "super-secret-api-key"
|
||||
if err := afero.WriteFile(fs, "/secrets/api-key.txt", []byte(secretContent), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a config that references the file
|
||||
configYAML := `
|
||||
environments:
|
||||
prod:
|
||||
secrets:
|
||||
apiKey: $FILE:/secrets/api-key.txt
|
||||
`
|
||||
if err := afero.WriteFile(fs, "config.yaml", []byte(configYAML), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Use the filesystem
|
||||
config.SetFs(fs)
|
||||
config.SetEnvironment("prod")
|
||||
|
||||
// Get the secret - it should resolve from the file
|
||||
apiKey := config.GetSecretString("apiKey")
|
||||
if apiKey != secretContent {
|
||||
t.Errorf("Expected %s, got %s", secretContent, apiKey)
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"sneak.berlin/go/webhooker/pkg/config"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
// Set the environment explicitly
|
||||
config.SetEnvironment("dev")
|
||||
|
||||
// Get configuration values
|
||||
baseURL := config.GetString("baseURL")
|
||||
timeout := config.GetInt("timeout", 30)
|
||||
debugMode := config.GetBool("debugMode", false)
|
||||
|
||||
fmt.Printf("Base URL: %s\n", baseURL)
|
||||
fmt.Printf("Timeout: %d\n", timeout)
|
||||
fmt.Printf("Debug Mode: %v\n", debugMode)
|
||||
|
||||
// Get secret values
|
||||
apiKey := config.GetSecretString("api_key")
|
||||
if apiKey != "" {
|
||||
fmt.Printf("API Key: %s...\n", apiKey[:8])
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSetEnvironment() {
|
||||
// Your application determines which environment to use
|
||||
// This could come from command line args, env vars, etc.
|
||||
environment := os.Getenv("APP_ENV")
|
||||
if environment == "" {
|
||||
environment = "development"
|
||||
}
|
||||
|
||||
// Set the environment explicitly
|
||||
config.SetEnvironment(environment)
|
||||
|
||||
// Now use configuration throughout your application
|
||||
fmt.Printf("Environment: %s\n", environment)
|
||||
fmt.Printf("App Name: %s\n", config.GetString("app_name"))
|
||||
}
|
||||
|
||||
func ExampleGetString() {
|
||||
config.SetEnvironment("prod")
|
||||
|
||||
// Get a string configuration value with a default
|
||||
baseURL := config.GetString("baseURL", "http://localhost:8080")
|
||||
fmt.Printf("Base URL: %s\n", baseURL)
|
||||
}
|
||||
|
||||
func ExampleGetInt() {
|
||||
config.SetEnvironment("prod")
|
||||
|
||||
// Get an integer configuration value with a default
|
||||
port := config.GetInt("port", 8080)
|
||||
fmt.Printf("Port: %d\n", port)
|
||||
}
|
||||
|
||||
func ExampleGetBool() {
|
||||
config.SetEnvironment("dev")
|
||||
|
||||
// Get a boolean configuration value with a default
|
||||
debugMode := config.GetBool("debugMode", false)
|
||||
fmt.Printf("Debug Mode: %v\n", debugMode)
|
||||
}
|
||||
|
||||
func ExampleGetSecretString() {
|
||||
config.SetEnvironment("prod")
|
||||
|
||||
// Get a secret string value
|
||||
apiKey := config.GetSecretString("api_key")
|
||||
if apiKey != "" {
|
||||
// Be careful not to log the full secret!
|
||||
fmt.Printf("API Key configured: yes\n")
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleLoadFile() {
|
||||
// Load configuration from a specific file
|
||||
if err := config.LoadFile("/path/to/config.yaml"); err != nil {
|
||||
log.Printf("Failed to load config: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config.SetEnvironment("staging")
|
||||
fmt.Printf("Loaded configuration from custom file\n")
|
||||
}
|
||||
|
||||
func ExampleReload() {
|
||||
config.SetEnvironment("dev")
|
||||
|
||||
// Get initial value
|
||||
oldValue := config.GetString("some_key")
|
||||
|
||||
// ... config file might have been updated ...
|
||||
|
||||
// Reload configuration from file
|
||||
if err := config.Reload(); err != nil {
|
||||
log.Printf("Failed to reload config: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get potentially updated value
|
||||
newValue := config.GetString("some_key")
|
||||
fmt.Printf("Value changed: %v\n", oldValue != newValue)
|
||||
}
|
||||
|
||||
// Example config.yaml structure:
|
||||
/*
|
||||
environments:
|
||||
development:
|
||||
config:
|
||||
baseURL: http://localhost:8000
|
||||
debugMode: true
|
||||
port: 8000
|
||||
secrets:
|
||||
api_key: dev-key-12345
|
||||
|
||||
production:
|
||||
config:
|
||||
baseURL: https://api.example.com
|
||||
debugMode: false
|
||||
port: 443
|
||||
GCPProject: my-project-123
|
||||
AWSRegion: us-west-2
|
||||
secrets:
|
||||
api_key: $GSM:prod-api-key
|
||||
db_password: $ASM:prod/db/password
|
||||
|
||||
configDefaults:
|
||||
app_name: My Application
|
||||
timeout: 30
|
||||
log_level: INFO
|
||||
port: 8080
|
||||
*/
|
||||
@@ -1,41 +0,0 @@
|
||||
module sneak.berlin/go/webhooker/pkg/config
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.50.0
|
||||
github.com/spf13/afero v1.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.23.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.3 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.13.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/api v0.149.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/secretmanager v1.11.4
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
)
|
||||
@@ -1,161 +0,0 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.110.8 h1:tyNdfIxjzaWctIiLYOTalaLKZ17SI44SKFW26QbOhME=
|
||||
cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk=
|
||||
cloud.google.com/go/compute v1.23.1 h1:V97tBoDaZHb6leicZ1G6DLK2BAaZLJ/7+9BB/En3hR0=
|
||||
cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/iam v1.1.3 h1:18tKG7DzydKWUnLjonWcJO6wjSCAtzh4GcRKlH/Hrzc=
|
||||
cloud.google.com/go/iam v1.1.3/go.mod h1:3khUlaBXfPKKe7huYgEpDn6FtgRyMEqbkvBxrQyY5SE=
|
||||
cloud.google.com/go/secretmanager v1.11.4 h1:krnX9qpG2kR2fJ+u+uNyNo+ACVhplIAS4Pu7u+4gd+k=
|
||||
cloud.google.com/go/secretmanager v1.11.4/go.mod h1:wreJlbS9Zdq21lMzWmJ0XhWW2ZxgPeahsqeV/vZoJ3w=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/aws/aws-sdk-go v1.50.0 h1:HBtrLeO+QyDKnc3t1+5DR1RxodOHCGr8ZcrHudpv7jI=
|
||||
github.com/aws/aws-sdk-go v1.50.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
|
||||
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY=
|
||||
google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA=
|
||||
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1,104 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Loader handles loading configuration from YAML files.
|
||||
type Loader struct {
|
||||
fs afero.Fs
|
||||
}
|
||||
|
||||
// NewLoader creates a new configuration loader.
|
||||
func NewLoader(fs afero.Fs) *Loader {
|
||||
return &Loader{
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
// FindConfigFile searches for a configuration file by looking up the directory tree.
|
||||
func (l *Loader) FindConfigFile(filename string) (string, error) {
|
||||
if filename == "" {
|
||||
filename = "config.yaml"
|
||||
}
|
||||
|
||||
// First check if the file exists in the current directory (simple case)
|
||||
if _, err := l.fs.Stat(filename); err == nil {
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
// For more complex cases, try to walk up the directory tree
|
||||
// Start from current directory or root for in-memory filesystems
|
||||
currentDir := "."
|
||||
|
||||
// Try to get the absolute path, but if it fails (e.g., in-memory fs),
|
||||
// just use the current directory
|
||||
if absPath, err := filepath.Abs("."); err == nil {
|
||||
currentDir = absPath
|
||||
}
|
||||
|
||||
// Search up the directory tree
|
||||
for {
|
||||
configPath := filepath.Join(currentDir, filename)
|
||||
if _, err := l.fs.Stat(configPath); err == nil {
|
||||
return configPath, nil
|
||||
}
|
||||
|
||||
// Move up one directory
|
||||
parentDir := filepath.Dir(currentDir)
|
||||
if parentDir == currentDir || currentDir == "." || currentDir == "/" {
|
||||
// Reached the root directory or can't go up further
|
||||
break
|
||||
}
|
||||
currentDir = parentDir
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("configuration file %s not found in directory tree", filename)
|
||||
}
|
||||
|
||||
// LoadYAML loads a YAML file and returns the parsed configuration.
|
||||
func (l *Loader) LoadYAML(filePath string) (map[string]interface{}, error) {
|
||||
data, err := afero.ReadFile(l.fs, filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
var config map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse YAML from %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
config = make(map[string]interface{})
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// MergeConfigs performs a deep merge of two configuration maps.
|
||||
// The override map values take precedence over the base map.
|
||||
func (l *Loader) MergeConfigs(base, override map[string]interface{}) map[string]interface{} {
|
||||
if base == nil {
|
||||
base = make(map[string]interface{})
|
||||
}
|
||||
|
||||
for key, value := range override {
|
||||
if baseValue, exists := base[key]; exists {
|
||||
// If both values are maps, merge them recursively
|
||||
if baseMap, baseOk := baseValue.(map[string]interface{}); baseOk {
|
||||
if overrideMap, overrideOk := value.(map[string]interface{}); overrideOk {
|
||||
base[key] = l.MergeConfigs(baseMap, overrideMap)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// Otherwise, override the value
|
||||
base[key] = value
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Manager manages application configuration with value resolution.
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
config map[string]interface{}
|
||||
environment string
|
||||
resolver *Resolver
|
||||
loader *Loader
|
||||
configFile string
|
||||
resolvedCache map[string]interface{}
|
||||
fs afero.Fs
|
||||
}
|
||||
|
||||
// NewManager creates a new configuration manager.
|
||||
func NewManager() *Manager {
|
||||
fs := afero.NewOsFs()
|
||||
return &Manager{
|
||||
config: make(map[string]interface{}),
|
||||
loader: NewLoader(fs),
|
||||
resolvedCache: make(map[string]interface{}),
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
// SetFs sets the filesystem to use for all file operations.
|
||||
// This is primarily useful for testing with an in-memory filesystem.
|
||||
func (m *Manager) SetFs(fs afero.Fs) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.fs = fs
|
||||
m.loader = NewLoader(fs)
|
||||
|
||||
// If we have a resolver, recreate it with the new fs
|
||||
if m.resolver != nil {
|
||||
gcpProject := ""
|
||||
awsRegion := "us-east-1"
|
||||
|
||||
// Try to get the current settings
|
||||
if gcpProj := m.getConfigValue("GCPProject", ""); gcpProj != nil {
|
||||
if str, ok := gcpProj.(string); ok {
|
||||
gcpProject = str
|
||||
}
|
||||
}
|
||||
if awsReg := m.getConfigValue("AWSRegion", "us-east-1"); awsReg != nil {
|
||||
if str, ok := awsReg.(string); ok {
|
||||
awsRegion = str
|
||||
}
|
||||
}
|
||||
|
||||
m.resolver = NewResolver(gcpProject, awsRegion, fs)
|
||||
}
|
||||
|
||||
// Clear caches as filesystem changed
|
||||
m.resolvedCache = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// LoadFile loads configuration from a specific file.
|
||||
func (m *Manager) LoadFile(configFile string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
config, err := m.loader.LoadYAML(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.config = config
|
||||
m.configFile = configFile
|
||||
m.resolvedCache = make(map[string]interface{}) // Clear cache
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadConfig loads the configuration from file.
|
||||
func (m *Manager) loadConfig() error {
|
||||
if m.configFile == "" {
|
||||
// Try to find config.yaml
|
||||
configPath, err := m.loader.FindConfigFile("config.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.configFile = configPath
|
||||
}
|
||||
|
||||
config, err := m.loader.LoadYAML(m.configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.config = config
|
||||
m.resolvedCache = make(map[string]interface{}) // Clear cache
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetEnvironment sets the active environment.
|
||||
func (m *Manager) SetEnvironment(environment string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.environment = strings.ToLower(environment)
|
||||
|
||||
// Create resolver with GCP project and AWS region if available
|
||||
gcpProject := m.getConfigValue("GCPProject", "")
|
||||
awsRegion := m.getConfigValue("AWSRegion", "us-east-1")
|
||||
|
||||
if gcpProjectStr, ok := gcpProject.(string); ok {
|
||||
if awsRegionStr, ok := awsRegion.(string); ok {
|
||||
m.resolver = NewResolver(gcpProjectStr, awsRegionStr, m.fs)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear resolved cache when environment changes
|
||||
m.resolvedCache = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Get retrieves a configuration value.
|
||||
func (m *Manager) Get(key string, defaultValue interface{}) interface{} {
|
||||
m.mu.RLock()
|
||||
|
||||
// Ensure config is loaded
|
||||
if m.config == nil || len(m.config) == 0 {
|
||||
// Need to upgrade to write lock to load config
|
||||
m.mu.RUnlock()
|
||||
m.mu.Lock()
|
||||
// Double-check after acquiring write lock
|
||||
if m.config == nil || len(m.config) == 0 {
|
||||
if err := m.loadConfig(); err != nil {
|
||||
// Config file not found is expected when all values
|
||||
// come from environment variables. Only log at debug
|
||||
// level to avoid confusing "Failed to load config"
|
||||
// messages during normal operation.
|
||||
_ = err
|
||||
m.mu.Unlock()
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
// Downgrade back to read lock
|
||||
m.mu.Unlock()
|
||||
m.mu.RLock()
|
||||
}
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// Check cache first
|
||||
cacheKey := fmt.Sprintf("config.%s", key)
|
||||
if cached, ok := m.resolvedCache[cacheKey]; ok {
|
||||
return cached
|
||||
}
|
||||
|
||||
// Try environment-specific config first
|
||||
var rawValue interface{}
|
||||
if m.environment != "" {
|
||||
envMap, ok := m.config["environments"].(map[string]interface{})
|
||||
if ok {
|
||||
if env, ok := envMap[m.environment].(map[string]interface{}); ok {
|
||||
if config, ok := env["config"].(map[string]interface{}); ok {
|
||||
if val, exists := config[key]; exists {
|
||||
rawValue = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to configDefaults
|
||||
if rawValue == nil {
|
||||
if defaults, ok := m.config["configDefaults"].(map[string]interface{}); ok {
|
||||
if val, exists := defaults[key]; exists {
|
||||
rawValue = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rawValue == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Resolve the value if we have a resolver
|
||||
var resolvedValue interface{}
|
||||
if m.resolver != nil {
|
||||
resolvedValue = m.resolver.Resolve(rawValue)
|
||||
} else {
|
||||
resolvedValue = rawValue
|
||||
}
|
||||
|
||||
// Cache the resolved value
|
||||
m.resolvedCache[cacheKey] = resolvedValue
|
||||
|
||||
return resolvedValue
|
||||
}
|
||||
|
||||
// GetSecret retrieves a secret value for the current environment.
|
||||
func (m *Manager) GetSecret(key string, defaultValue interface{}) interface{} {
|
||||
m.mu.RLock()
|
||||
|
||||
// Ensure config is loaded
|
||||
if m.config == nil || len(m.config) == 0 {
|
||||
// Need to upgrade to write lock to load config
|
||||
m.mu.RUnlock()
|
||||
m.mu.Lock()
|
||||
// Double-check after acquiring write lock
|
||||
if m.config == nil || len(m.config) == 0 {
|
||||
if err := m.loadConfig(); err != nil {
|
||||
// Config file not found is expected when all values
|
||||
// come from environment variables.
|
||||
_ = err
|
||||
m.mu.Unlock()
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
// Downgrade back to read lock
|
||||
m.mu.Unlock()
|
||||
m.mu.RLock()
|
||||
}
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
if m.environment == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Get the current environment's config
|
||||
envMap, ok := m.config["environments"].(map[string]interface{})
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
env, ok := envMap[m.environment].(map[string]interface{})
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
secrets, ok := env["secrets"].(map[string]interface{})
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
secretValue, exists := secrets[key]
|
||||
if !exists {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Resolve the value
|
||||
if m.resolver != nil {
|
||||
resolved := m.resolver.Resolve(secretValue)
|
||||
if resolved == nil {
|
||||
return defaultValue
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
return secretValue
|
||||
}
|
||||
|
||||
// getConfigValue is an internal helper to get config values without locking.
|
||||
func (m *Manager) getConfigValue(key string, defaultValue interface{}) interface{} {
|
||||
// Try environment-specific config first
|
||||
var rawValue interface{}
|
||||
if m.environment != "" {
|
||||
envMap, ok := m.config["environments"].(map[string]interface{})
|
||||
if ok {
|
||||
if env, ok := envMap[m.environment].(map[string]interface{}); ok {
|
||||
if config, ok := env["config"].(map[string]interface{}); ok {
|
||||
if val, exists := config[key]; exists {
|
||||
rawValue = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to configDefaults
|
||||
if rawValue == nil {
|
||||
if defaults, ok := m.config["configDefaults"].(map[string]interface{}); ok {
|
||||
if val, exists := defaults[key]; exists {
|
||||
rawValue = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rawValue == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return rawValue
|
||||
}
|
||||
|
||||
// Reload reloads the configuration from file.
|
||||
func (m *Manager) Reload() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
return m.loadConfig()
|
||||
}
|
||||
|
||||
// GetAllConfig returns all configuration values for the current environment.
|
||||
func (m *Manager) GetAllConfig() map[string]interface{} {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// Start with configDefaults
|
||||
if defaults, ok := m.config["configDefaults"].(map[string]interface{}); ok {
|
||||
for k, v := range defaults {
|
||||
if m.resolver != nil {
|
||||
result[k] = m.resolver.Resolve(v)
|
||||
} else {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override with environment-specific config
|
||||
if m.environment != "" {
|
||||
envMap, ok := m.config["environments"].(map[string]interface{})
|
||||
if ok {
|
||||
if env, ok := envMap[m.environment].(map[string]interface{}); ok {
|
||||
if config, ok := env["config"].(map[string]interface{}); ok {
|
||||
for k, v := range config {
|
||||
if m.resolver != nil {
|
||||
result[k] = m.resolver.Resolve(v)
|
||||
} else {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAllSecrets returns all secrets for the current environment.
|
||||
func (m *Manager) GetAllSecrets() map[string]interface{} {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
if m.environment == "" {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
envMap, ok := m.config["environments"].(map[string]interface{})
|
||||
if !ok {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
env, ok := envMap[m.environment].(map[string]interface{})
|
||||
if !ok {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
secrets, ok := env["secrets"].(map[string]interface{})
|
||||
if !ok {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Resolve all secrets
|
||||
result := make(map[string]interface{})
|
||||
for k, v := range secrets {
|
||||
if m.resolver != nil {
|
||||
result[k] = m.resolver.Resolve(v)
|
||||
} else {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
secretmanager "cloud.google.com/go/secretmanager/apiv1"
|
||||
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/secretsmanager"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Resolver handles resolution of configuration values with special prefixes.
|
||||
type Resolver struct {
|
||||
gcpProject string
|
||||
awsRegion string
|
||||
gsmClient *secretmanager.Client
|
||||
asmClient *secretsmanager.SecretsManager
|
||||
awsSession *session.Session
|
||||
specialValue *regexp.Regexp
|
||||
fs afero.Fs
|
||||
}
|
||||
|
||||
// NewResolver creates a new value resolver.
|
||||
func NewResolver(gcpProject, awsRegion string, fs afero.Fs) *Resolver {
|
||||
return &Resolver{
|
||||
gcpProject: gcpProject,
|
||||
awsRegion: awsRegion,
|
||||
specialValue: regexp.MustCompile(`^\$([A-Z]+):(.+)$`),
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve resolves a configuration value that may contain special prefixes.
|
||||
func (r *Resolver) Resolve(value interface{}) interface{} {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return r.resolveString(v)
|
||||
case map[string]interface{}:
|
||||
// Recursively resolve map values
|
||||
result := make(map[string]interface{})
|
||||
for k, val := range v {
|
||||
result[k] = r.Resolve(val)
|
||||
}
|
||||
return result
|
||||
case []interface{}:
|
||||
// Recursively resolve slice items
|
||||
result := make([]interface{}, len(v))
|
||||
for i, val := range v {
|
||||
result[i] = r.Resolve(val)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
// Return non-string values as-is
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// resolveString resolves a string value that may contain a special prefix.
|
||||
func (r *Resolver) resolveString(value string) interface{} {
|
||||
matches := r.specialValue.FindStringSubmatch(value)
|
||||
if matches == nil {
|
||||
return value
|
||||
}
|
||||
|
||||
resolverType := matches[1]
|
||||
resolverValue := matches[2]
|
||||
|
||||
switch resolverType {
|
||||
case "ENV":
|
||||
return r.resolveEnv(resolverValue)
|
||||
case "GSM":
|
||||
return r.resolveGSM(resolverValue)
|
||||
case "ASM":
|
||||
return r.resolveASM(resolverValue)
|
||||
case "FILE":
|
||||
return r.resolveFile(resolverValue)
|
||||
default:
|
||||
log.Printf("Unknown resolver type: %s", resolverType)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// resolveEnv resolves an environment variable.
|
||||
func (r *Resolver) resolveEnv(envVar string) interface{} {
|
||||
value := os.Getenv(envVar)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// resolveGSM resolves a Google Secret Manager secret.
|
||||
func (r *Resolver) resolveGSM(secretName string) interface{} {
|
||||
if r.gcpProject == "" {
|
||||
log.Printf("GCP project not configured for GSM resolution")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize GSM client if needed
|
||||
if r.gsmClient == nil {
|
||||
ctx := context.Background()
|
||||
client, err := secretmanager.NewClient(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create GSM client: %v", err)
|
||||
return nil
|
||||
}
|
||||
r.gsmClient = client
|
||||
}
|
||||
|
||||
// Build the resource name
|
||||
name := fmt.Sprintf("projects/%s/secrets/%s/versions/latest", r.gcpProject, secretName)
|
||||
|
||||
// Access the secret
|
||||
ctx := context.Background()
|
||||
req := &secretmanagerpb.AccessSecretVersionRequest{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
result, err := r.gsmClient.AccessSecretVersion(ctx, req)
|
||||
if err != nil {
|
||||
log.Printf("Failed to access GSM secret %s: %v", secretName, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return string(result.Payload.Data)
|
||||
}
|
||||
|
||||
// resolveASM resolves an AWS Secrets Manager secret.
|
||||
func (r *Resolver) resolveASM(secretName string) interface{} {
|
||||
// Initialize AWS session if needed
|
||||
if r.awsSession == nil {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(r.awsRegion),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to create AWS session: %v", err)
|
||||
return nil
|
||||
}
|
||||
r.awsSession = sess
|
||||
}
|
||||
|
||||
// Initialize ASM client if needed
|
||||
if r.asmClient == nil {
|
||||
r.asmClient = secretsmanager.New(r.awsSession)
|
||||
}
|
||||
|
||||
// Get the secret value
|
||||
input := &secretsmanager.GetSecretValueInput{
|
||||
SecretId: aws.String(secretName),
|
||||
}
|
||||
|
||||
result, err := r.asmClient.GetSecretValue(input)
|
||||
if err != nil {
|
||||
log.Printf("Failed to access ASM secret %s: %v", secretName, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return the secret string
|
||||
if result.SecretString != nil {
|
||||
return *result.SecretString
|
||||
}
|
||||
|
||||
// If it's binary data, we can't handle it as a string config value
|
||||
log.Printf("ASM secret %s contains binary data, which is not supported", secretName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveFile resolves a file's contents.
|
||||
func (r *Resolver) resolveFile(filePath string) interface{} {
|
||||
// Expand user home directory if present
|
||||
if strings.HasPrefix(filePath, "~/") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get user home directory: %v", err)
|
||||
return nil
|
||||
}
|
||||
filePath = filepath.Join(home, filePath[2:])
|
||||
}
|
||||
|
||||
data, err := afero.ReadFile(r.fs, filePath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to read file %s: %v", filePath, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Strip whitespace/newlines from file contents
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
// Close closes any open clients.
|
||||
func (r *Resolver) Close() error {
|
||||
if r.gsmClient != nil {
|
||||
return r.gsmClient.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user