Skip to content

Commit 3f78c6b

Browse files
authored
Merge pull request #140 from ardens-jw/master
Feature: Provide support for RDS MySQL IAM Authentication
2 parents 62daa91 + 512daaa commit 3f78c6b

3 files changed

Lines changed: 105 additions & 15 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@ For some database backends some special functionality is available:
223223
which will use the equivalent of `rds generate-db-auth-token`
224224
for the password. For this driver, the `AWS_REGION` environment variable
225225
must be set.
226+
* rds-mysql: This type of URL expects a working AWS configuration
227+
which will use the equivalent of `rds generate-db-auth-token`
228+
for the password. For this driver, the `AWS_REGION` environment variable
229+
must be set.
230+
226231
227232
Why this exporter exists
228233
========================

config.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,13 @@ type Job struct {
155155
}
156156

157157
type connection struct {
158-
conn *sqlx.DB
159-
url string
160-
driver string
161-
host string
162-
database string
163-
user string
158+
conn *sqlx.DB
159+
url string
160+
driver string
161+
host string
162+
database string
163+
user string
164+
tokenExpirationTime time.Time
164165
}
165166

166167
// Query is an SQL query that is executed on a connection

job.go

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,27 @@ var (
3737
CloudSQLPrefix = "cloudsql+"
3838
)
3939

40+
func handleRDSMySQLIAMAuth(conn string) (string, time.Time, error) {
41+
dsn := strings.TrimPrefix(conn, "rds-mysql://")
42+
config, err := mysql.ParseDSN(dsn)
43+
if err != nil {
44+
return "", time.Time{}, fmt.Errorf("failed to parse MySQL DSN: %v", err)
45+
}
46+
47+
sess := session.Must(session.NewSessionWithOptions(session.Options{
48+
SharedConfigState: session.SharedConfigEnable,
49+
}))
50+
51+
token, err := rdsutils.BuildAuthToken(config.Addr, os.Getenv("AWS_REGION"), config.User, sess.Config.Credentials)
52+
if err != nil {
53+
return "", time.Time{}, fmt.Errorf("failed to build RDS auth token: %v", err)
54+
}
55+
56+
expirationTime := time.Now().Add(14 * time.Minute)
57+
58+
return token, expirationTime, nil
59+
}
60+
4061
// Init will initialize the metric descriptors
4162
func (j *Job) Init(logger log.Logger, queries map[string]string) error {
4263
j.log = log.With(logger, "job", j.Name)
@@ -207,23 +228,53 @@ func (j *Job) updateConnections() {
207228
continue
208229
}
209230

210-
// MySQL DSNs do not parse cleanly as URLs as of Go 1.12.8+
211-
if strings.HasPrefix(conn, "mysql://") {
212-
config, err := mysql.ParseDSN(strings.TrimPrefix(conn, "mysql://"))
231+
// Handle both RDS MySQL and regular MySQL connections
232+
if strings.HasPrefix(conn, "rds-mysql://") || strings.HasPrefix(conn, "mysql://") {
233+
isRDS := strings.HasPrefix(conn, "rds-mysql://")
234+
var dsn string
235+
var expirationTime time.Time
236+
237+
trimmedConn := conn
238+
if isRDS {
239+
trimmedConn = strings.TrimPrefix(conn, "rds-mysql://")
240+
} else {
241+
trimmedConn = strings.TrimPrefix(conn, "mysql://")
242+
}
243+
244+
config, err := mysql.ParseDSN(trimmedConn)
213245
if err != nil {
214246
level.Error(j.log).Log("msg", "Failed to parse MySQL DSN", "url", conn, "err", err)
247+
continue
248+
}
249+
250+
if isRDS {
251+
authToken, tokenExpiration, err := handleRDSMySQLIAMAuth(conn)
252+
if err != nil {
253+
level.Error(j.log).Log("msg", "Failed to build RDS auth token", "url", conn, "err", err)
254+
continue
255+
}
256+
config.Passwd = authToken
257+
config.AllowCleartextPasswords = true
258+
expirationTime = tokenExpiration
259+
}
260+
261+
dsn = config.FormatDSN()
262+
if isRDS {
263+
dsn = "rds-mysql://" + dsn
215264
}
216265

217266
j.conns = append(j.conns, &connection{
218-
conn: nil,
219-
url: conn,
220-
driver: "mysql",
221-
host: config.Addr,
222-
database: config.DBName,
223-
user: config.User,
267+
conn: nil,
268+
url: dsn,
269+
driver: "mysql",
270+
host: config.Addr,
271+
database: config.DBName,
272+
user: config.User,
273+
tokenExpirationTime: expirationTime,
224274
})
225275
continue
226276
}
277+
227278
if strings.HasPrefix(conn, "rds-postgres://") {
228279
// Reuse Postgres driver by stripping "rds-" from connection URL after building the RDS authentication token
229280
conn = strings.TrimPrefix(conn, "rds-")
@@ -438,12 +489,45 @@ func (j *Job) runOnce() error {
438489
func (c *connection) connect(job *Job) error {
439490
// already connected
440491
if c.conn != nil {
492+
if strings.HasPrefix(c.url, "rds-mysql://") && time.Now().After(c.tokenExpirationTime) {
493+
level.Warn(job.log).Log("msg", "Connection token expired, reconnecting")
494+
495+
authToken, expirationTime, err := handleRDSMySQLIAMAuth(c.url)
496+
if err != nil {
497+
return fmt.Errorf("failed to refresh RDS MySQL IAM Auth token: %w", err)
498+
}
499+
500+
config, err := mysql.ParseDSN(strings.TrimPrefix(c.url, "rds-mysql://"))
501+
if err != nil {
502+
return fmt.Errorf("failed to parse MySQL DSN: %w", err)
503+
}
504+
505+
config.Passwd = authToken
506+
dsn := "rds-mysql://" + config.FormatDSN()
507+
508+
// Close the existing connection
509+
c.conn.Close()
510+
c.conn = nil
511+
512+
// Update the connection details
513+
c.tokenExpirationTime = expirationTime
514+
c.url = dsn
515+
516+
// Connect to the database with the new token
517+
conn, err := sqlx.Connect(c.driver, strings.TrimPrefix(dsn, "rds-mysql://"))
518+
if err != nil {
519+
return fmt.Errorf("failed to connect to the database: %w", err)
520+
}
521+
c.conn = conn
522+
return nil
523+
}
441524
return nil
442525
}
443526
dsn := c.url
444527
switch c.driver {
445528
case "mysql":
446529
dsn = strings.TrimPrefix(dsn, "mysql://")
530+
dsn = strings.TrimPrefix(dsn, "rds-mysql://")
447531
case "clickhouse+tcp", "clickhouse+http": // Support both http and tcp connections
448532
dsn = strings.TrimPrefix(dsn, "clickhouse+")
449533
c.driver = "clickhouse"

0 commit comments

Comments
 (0)