/
data_stores.go
219 lines (185 loc) · 7.47 KB
/
data_stores.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package sdks
import (
"context"
"errors"
"strings"
"github.com/launchdarkly/ld-relay/v7/config"
"github.com/launchdarkly/ld-relay/v7/internal/util"
"github.com/launchdarkly/go-sdk-common/v3/ldlog"
ldconsul "github.com/launchdarkly/go-server-sdk-consul/v2"
lddynamodb "github.com/launchdarkly/go-server-sdk-dynamodb/v3"
ldredis "github.com/launchdarkly/go-server-sdk-redis-redigo/v2"
"github.com/launchdarkly/go-server-sdk/v6/ldcomponents"
"github.com/launchdarkly/go-server-sdk/v6/subsystems"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
redigo "github.com/gomodule/redigo/redis"
consul "github.com/hashicorp/consul/api"
)
var (
errDynamoDBWithNoTableName = errors.New("TableName property must be specified for DynamoDB, either globally or per environment")
)
// DataStoreEnvironmentInfo encapsulates database-related configuration details that we will expose in the
// status resource for a specific environment. Some of these are set on a per-environment basis and others
// are global.
type DataStoreEnvironmentInfo struct {
// DBType is the type of database Relay is using, or "" for the default in-memory storage.
DBType string
// DBServer is the URL or host address of the database server, if applicable. Passwords, if any,
// must be redacted in this string.
DBServer string
// DBPrefix is the key prefix used for this environment to distinguish it from data that might be in
// the same database for other environments. This is required for Redis and Consul but optional for
// DynamoDB.
DBPrefix string
// DBTable is the table name for this environment if using DynamoDB, or "" otherwise.
DBTable string
}
// ConfigureDataStore provides the appropriate Go SDK data store factory (in-memory, Redis, etc.) based on
// the Relay configuration. It can return an error for some invalid configurations, but it assumes that we
// have already done the standard validation steps defined in the config package.
func ConfigureDataStore(
allConfig config.Config,
envConfig config.EnvConfig,
loggers ldlog.Loggers,
) (subsystems.ComponentConfigurer[subsystems.DataStore], DataStoreEnvironmentInfo, error) {
if allConfig.Redis.URL.IsDefined() {
// Our config validation already takes care of normalizing the Redis parameters so that if a
// host & port were specified, they are transformed into a URL.
redisBuilder, redisURL := makeRedisDataStoreBuilder(ldredis.DataStore, allConfig, envConfig)
redactedURL := util.RedactURL(redisURL)
loggers.Infof("Using Redis data store: %s with prefix: %s", redactedURL, envConfig.Prefix)
storeInfo := DataStoreEnvironmentInfo{
DBType: "redis",
DBServer: redactedURL,
DBPrefix: envConfig.Prefix,
}
if storeInfo.DBPrefix == "" {
storeInfo.DBPrefix = ldredis.DefaultPrefix
}
return ldcomponents.PersistentDataStore(redisBuilder).
CacheTime(allConfig.Redis.LocalTTL.GetOrElse(config.DefaultDatabaseCacheTTL)), storeInfo, nil
}
if allConfig.Consul.Host != "" {
dbConfig := allConfig.Consul
loggers.Infof("Using Consul data store: %s with prefix: %s", dbConfig.Host, envConfig.Prefix)
builder := ldconsul.DataStore().
Prefix(envConfig.Prefix)
if dbConfig.Token != "" {
builder.Config(consul.Config{Token: dbConfig.Token})
} else if dbConfig.TokenFile != "" {
builder.Config(consul.Config{TokenFile: dbConfig.TokenFile})
}
builder.Address(dbConfig.Host) // this is deliberately done last so it's not overridden by builder.Config()
storeInfo := DataStoreEnvironmentInfo{
DBType: "consul",
DBServer: dbConfig.Host,
DBPrefix: envConfig.Prefix,
}
if storeInfo.DBPrefix == "" {
storeInfo.DBPrefix = ldconsul.DefaultPrefix
}
return ldcomponents.PersistentDataStore(builder).
CacheTime(dbConfig.LocalTTL.GetOrElse(config.DefaultDatabaseCacheTTL)), storeInfo, nil
}
if allConfig.DynamoDB.Enabled {
builder, tableName, err := makeDynamoDBDataStoreBuilder(lddynamodb.DataStore, allConfig, envConfig)
if err != nil {
return nil, DataStoreEnvironmentInfo{}, err
}
loggers.Infof("Using DynamoDB data store: %s with prefix: %s", tableName, envConfig.Prefix)
storeInfo := DataStoreEnvironmentInfo{
DBType: "dynamodb",
DBServer: allConfig.DynamoDB.URL.String(),
DBPrefix: envConfig.Prefix,
DBTable: tableName,
}
return ldcomponents.PersistentDataStore(builder).
CacheTime(allConfig.DynamoDB.LocalTTL.GetOrElse(config.DefaultDatabaseCacheTTL)), storeInfo, nil
}
return ldcomponents.InMemoryDataStore(), DataStoreEnvironmentInfo{}, nil
}
// GetRedisBasicProperties transforms the configuration properties to the standard parameters
// used for Redis. This function is exported to ensure consistency between the SDK
// configuration and the internal big segment store for Redis.
func GetRedisBasicProperties(
dbConfig config.RedisConfig,
envConfig config.EnvConfig,
) (redisURL, prefix string) {
redisURL = dbConfig.URL.String()
if dbConfig.TLS {
if strings.HasPrefix(redisURL, "redis:") {
// Redigo's DialUseTLS option will not work if you're specifying a URL.
redisURL = "rediss:" + strings.TrimPrefix(redisURL, "redis:")
}
}
prefix = envConfig.Prefix
if prefix == "" {
prefix = ldredis.DefaultPrefix
}
return
}
func makeRedisDataStoreBuilder[T any](
constructor func() *ldredis.StoreBuilder[T],
allConfig config.Config,
envConfig config.EnvConfig,
) (builder *ldredis.StoreBuilder[T], url string) {
redisURL, prefix := GetRedisBasicProperties(allConfig.Redis, envConfig)
var dialOptions []redigo.DialOption
if allConfig.Redis.Password != "" {
dialOptions = append(dialOptions, redigo.DialPassword(allConfig.Redis.Password))
}
if allConfig.Redis.Username != "" {
dialOptions = append(dialOptions, redigo.DialUsername(allConfig.Redis.Username))
}
b := constructor().
URL(redisURL).
Prefix(prefix).
DialOptions(dialOptions...)
return b, redisURL
}
// GetDynamoDBBasicProperties transforms the configuration properties to the standard parameters
// used for DynamoDB. This function is exported to ensure consistency between the SDK
// configuration and the internal big segment store for DynamoDB.
func GetDynamoDBBasicProperties(
dbConfig config.DynamoDBConfig,
envConfig config.EnvConfig,
) (endpoint *string, tableName, prefix string) {
// Note that the global TableName can be omitted if you specify a TableName for each environment
// (this is why we need an Enabled property here, since the other properties are all optional).
// You can also specify a prefix for each environment, as with the other databases.
tableName = envConfig.TableName
if tableName == "" {
tableName = dbConfig.TableName
}
prefix = envConfig.Prefix
if dbConfig.URL.IsDefined() {
endpoint = aws.String(dbConfig.URL.String())
}
return
}
func makeDynamoDBDataStoreBuilder[T any](
constructor func(string) *lddynamodb.StoreBuilder[T],
allConfig config.Config,
envConfig config.EnvConfig,
) (*lddynamodb.StoreBuilder[T], string, error) {
endpoint, tableName, prefix := GetDynamoDBBasicProperties(allConfig.DynamoDB, envConfig)
if tableName == "" {
return nil, "", errDynamoDBWithNoTableName
}
builder := constructor(tableName).
Prefix(prefix)
config, err := awsconfig.LoadDefaultConfig(context.Background())
if err != nil {
return nil, "", err
}
var options []func(*dynamodb.Options)
if endpoint != nil {
options = append(options, func(o *dynamodb.Options) {
o.EndpointResolver = dynamodb.EndpointResolverFromURL(*endpoint)
})
}
builder.ClientConfig(config, options...)
return builder, tableName, nil
}