diff --git a/.gitignore b/.gitignore index dc5ce28..21835e3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ testdata/ restql.yml -.DS_Store \ No newline at end of file +.DS_Store +.vscode/ diff --git a/docs/restql/config.md b/docs/restql/config.md index 8a3c0e3..ce22218 100644 --- a/docs/restql/config.md +++ b/docs/restql/config.md @@ -50,6 +50,25 @@ You can use the `pprof` tool to investigate restQL performance. To enable it set - Request ID: this middleware generates a unique id for each request restQL API receives. The `http.server.middlewares.requestId.header` field define the header name use to return the generated id. The `http.server.middlewares.requestId.strategy` defines how the id will be generated and can be either `base64` or `uuid`. - Timeout: this middleware limits the maximum time any request can take. The `http.server.middlewares.timeout.duration` field accept a time duration value. - Request Cancellation: this middleware stops query execution when the client drops the connection. This improves fault response as it avoids unnecessary computation and reduces traffic on downstream APIs. You can also manage the connection watching interval with the field `http.server.middlewares.requestCancellation.watchingInterval`, which accepts a duration string. +- Tenant By Host (From version 6.7.0): this middleware allows to configure default tenants by host. + You can configure your own tenants via the configuration file: + ```yaml + http: + server: + middlewares: + tenantByHost: + enable: true + defaultTenant: acom-npf + tenantsByHost: + "americanas.teste": acom-npf + "localhost": dev + ``` + Or via environment variables: + ```shell script + RESTQL_TENANT_BY_HOST_ENABLED=${enable} + RESTQL_TENANT_BY_HOST_DEFAULT_TENANT=${default_tenant} + RESTQL_TENANT_BY_HOST_MAP=${host_tenant_pairs} + ``` - CORS: Cross-Origin Resource Sharing is a specification that enables truly open access across domain-boundaries. You can configure your own CORS headers either via the configuration file: ```yaml diff --git a/internal/platform/conf/conf.go b/internal/platform/conf/conf.go index 52606fd..11e0c72 100644 --- a/internal/platform/conf/conf.go +++ b/internal/platform/conf/conf.go @@ -1,14 +1,15 @@ package conf import ( - "github.com/caarlos0/env/v6" - "gopkg.in/yaml.v2" "io/ioutil" "log" "os" "path/filepath" "strings" "time" + + "github.com/caarlos0/env/v6" + "gopkg.in/yaml.v2" ) const configFileName = "restql.yml" @@ -39,6 +40,12 @@ type requestCancellationConf struct { WatchInterval time.Duration `yaml:"watchInterval"` } +type tenantByHostConf struct { + Enable bool `yaml:"enable" env:"RESTQL_TENANT_BY_HOST_ENABLED"` + DefaultTenant string `yaml:"defaultTenant" env:"RESTQL_TENANT_BY_HOST_DEFAULT_TENANT"` + TenantsByHost map[string]string `yaml:"tenantsByHost" env:"RESTQL_TENANT_BY_HOST_MAP"` +} + // Config represents all parameters allowed in restQL runtime. type Config struct { HTTP struct { @@ -67,6 +74,7 @@ type Config struct { Timeout timeoutConf `yaml:"timeout"` Cors corsConf `yaml:"cors"` RequestCancellation requestCancellationConf `yaml:"requestCancellation"` + TenantByHost tenantByHostConf `yaml:"tenantByHost"` } `yaml:"middlewares"` } `yaml:"server"` diff --git a/internal/platform/web/middleware/middleware.go b/internal/platform/web/middleware/middleware.go index b8249b0..8f719b9 100644 --- a/internal/platform/web/middleware/middleware.go +++ b/internal/platform/web/middleware/middleware.go @@ -2,6 +2,7 @@ package middleware import ( "fmt" + "github.com/b2wdigital/restQL-golang/v6/internal/platform/conf" "github.com/b2wdigital/restQL-golang/v6/internal/platform/plugins" "github.com/b2wdigital/restQL-golang/v6/pkg/restql" @@ -69,6 +70,11 @@ func (d *Decorator) fetchEnabled() []Middleware { mws = append(mws, newRequestID(mwCfg.RequestID.Header, mwCfg.RequestID.Strategy, d.log)) } + if mwCfg.TenantByHost.Enable { + d.log.Info("url tenant middleware enabled") + mws = append(mws, newTenantByHost(d.log, mwCfg.TenantByHost.DefaultTenant, mwCfg.TenantByHost.TenantsByHost)) + } + if mwCfg.Cors.Enable { cors := newCors(d.log, corsOptions{ AllowedOrigins: mwCfg.Cors.AllowOrigin, diff --git a/internal/platform/web/middleware/tenant_by_host.go b/internal/platform/web/middleware/tenant_by_host.go new file mode 100644 index 0000000..3980100 --- /dev/null +++ b/internal/platform/web/middleware/tenant_by_host.go @@ -0,0 +1,48 @@ +package middleware + +import ( + "strings" + + "github.com/b2wdigital/restQL-golang/v6/pkg/restql" + + "github.com/valyala/fasthttp" +) + +type tenantByHost struct { + log restql.Logger + tenantsByHost map[string]string + defaultTenant string +} + +func newTenantByHost(log restql.Logger, defaultTenant string, tenantsByHost map[string]string) Middleware { + return tenantByHost{ + log: log, + tenantsByHost: tenantsByHost, + defaultTenant: defaultTenant, + } +} + +func (r tenantByHost) Apply(h fasthttp.RequestHandler) fasthttp.RequestHandler { + return func(ctx *fasthttp.RequestCtx) { + r.setTenant(ctx) + h(ctx) + } +} + +func (u tenantByHost) setTenant(ctx *fasthttp.RequestCtx) { + if string(ctx.QueryArgs().Peek("tenant")) != "" { + u.log.Debug("tenant already set", "tenant", string(ctx.QueryArgs().Peek("tenant"))) + return + } + + for k, v := range u.tenantsByHost { + if strings.Contains(string(ctx.Request.Host()), k) { + u.log.Debug("setting tenant", "tenant", v, "host", string(ctx.Request.Host())) + ctx.QueryArgs().Set("tenant", v) + return + } + } + u.log.Debug("setting default tenant", "tenant", u.defaultTenant, "host", string(ctx.Request.Host())) + ctx.QueryArgs().Set("tenant", u.defaultTenant) + +} diff --git a/internal/platform/web/middleware/tenant_by_host_test.go b/internal/platform/web/middleware/tenant_by_host_test.go new file mode 100644 index 0000000..e5fc101 --- /dev/null +++ b/internal/platform/web/middleware/tenant_by_host_test.go @@ -0,0 +1,87 @@ +package middleware + +import ( + "testing" + + "github.com/b2wdigital/restQL-golang/v6/pkg/restql" + "github.com/valyala/fasthttp" +) + +func Test_tenantByHost_setTenant(t *testing.T) { + type fields struct { + log restql.Logger + tenantsByHosts map[string]string + defaultTenant string + } + tests := []struct { + name string + fields fields + host string + tenant string + wantTenant string + }{ + { + name: "should set tenant", + fields: fields{ + log: noOpLogger{}, + tenantsByHosts: map[string]string{ + "americanas.test": "acom-npf", + }, + defaultTenant: "default-tenant", + }, + host: "americanas.test", + wantTenant: "acom-npf", + }, + { + name: "should not set tenant if already set", + fields: fields{ + log: noOpLogger{}, + tenantsByHosts: map[string]string{ + "americanas.test": "acom-npf", + }, + defaultTenant: "default-tenant", + }, + tenant: "previous-set-tenant", + host: "americanas.test", + wantTenant: "previous-set-tenant", + }, + { + name: "should set default tenant", + fields: fields{ + log: noOpLogger{}, + tenantsByHosts: map[string]string{ + "americanas.test": "acom-npf", + }, + defaultTenant: "default-tenant", + }, + host: "unknown.test", + wantTenant: "default-tenant", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := tenantByHost{ + log: tt.fields.log, + tenantsByHost: tt.fields.tenantsByHosts, + defaultTenant: tt.fields.defaultTenant, + } + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetHost(tt.host) + ctx.QueryArgs().Add("tenant", tt.tenant) + u.setTenant(ctx) + if got := string(ctx.QueryArgs().Peek("tenant")); got != tt.wantTenant { + t.Errorf("tenantByHost.setTenant() = %v, want %v", got, tt.wantTenant) + } + }) + } +} + +type noOpLogger struct{} + +func (n noOpLogger) Panic(msg string, fields ...interface{}) {} +func (n noOpLogger) Fatal(msg string, fields ...interface{}) {} +func (n noOpLogger) Error(msg string, err error, fields ...interface{}) {} +func (n noOpLogger) Warn(msg string, fields ...interface{}) {} +func (n noOpLogger) Info(msg string, fields ...interface{}) {} +func (n noOpLogger) Debug(msg string, fields ...interface{}) {} +func (n noOpLogger) With(key string, value interface{}) restql.Logger { return n }