github.com/iron-io/functions@v0.0.0-20180820112432-d59d7d1c40b2/api/datastore/mysql/mysql.go (about) 1 package mysql 2 3 import ( 4 "bytes" 5 "context" 6 "database/sql" 7 "encoding/json" 8 "fmt" 9 "net/url" 10 11 "github.com/Sirupsen/logrus" 12 "github.com/go-sql-driver/mysql" 13 _ "github.com/go-sql-driver/mysql" 14 "github.com/iron-io/functions/api/datastore/internal/datastoreutil" 15 "github.com/iron-io/functions/api/models" 16 ) 17 18 const routesTableCreate = `CREATE TABLE IF NOT EXISTS routes ( 19 app_name varchar(256) NOT NULL, 20 path varchar(256) NOT NULL, 21 image varchar(256) NOT NULL, 22 format varchar(16) NOT NULL, 23 maxc int NOT NULL, 24 memory int NOT NULL, 25 timeout int NOT NULL, 26 idle_timeout int NOT NULL, 27 type varchar(16) NOT NULL, 28 headers text NOT NULL, 29 config text NOT NULL, 30 PRIMARY KEY (app_name, path) 31 );` 32 33 const appsTableCreate = `CREATE TABLE IF NOT EXISTS apps ( 34 name varchar(256) NOT NULL PRIMARY KEY, 35 config text NOT NULL 36 );` 37 38 const extrasTableCreate = `CREATE TABLE IF NOT EXISTS extras ( 39 id varchar(256) NOT NULL PRIMARY KEY, 40 value varchar(256) NOT NULL 41 );` 42 43 const routeSelector = `SELECT app_name, path, image, format, maxc, memory, type, timeout, idle_timeout, headers, config FROM routes` 44 45 type rowScanner interface { 46 Scan(dest ...interface{}) error 47 } 48 49 type rowQuerier interface { 50 QueryRow(query string, args ...interface{}) *sql.Row 51 } 52 53 /* 54 MySQLDatastore defines a basic MySQL Datastore struct. 55 */ 56 type MySQLDatastore struct { 57 db *sql.DB 58 } 59 60 /* 61 New creates a new MySQL Datastore. 62 */ 63 func New(url *url.URL) (models.Datastore, error) { 64 u := fmt.Sprintf("%s@%s%s", url.User.String(), url.Host, url.Path) 65 db, err := sql.Open("mysql", u) 66 if err != nil { 67 return nil, err 68 } 69 70 err = db.Ping() 71 if err != nil { 72 return nil, err 73 } 74 75 maxIdleConns := 30 76 db.SetMaxIdleConns(maxIdleConns) 77 logrus.WithFields(logrus.Fields{"max_idle_connections": maxIdleConns}).Info("MySQL dialed") 78 79 pg := &MySQLDatastore{ 80 db: db, 81 } 82 83 for _, v := range []string{routesTableCreate, appsTableCreate, extrasTableCreate} { 84 _, err = db.Exec(v) 85 if err != nil { 86 return nil, err 87 } 88 } 89 90 return datastoreutil.NewValidator(pg), nil 91 } 92 93 /* 94 InsertApp inserts an app to MySQL. 95 */ 96 func (ds *MySQLDatastore) InsertApp(ctx context.Context, app *models.App) (*models.App, error) { 97 var cbyte []byte 98 var err error 99 100 if app.Config != nil { 101 cbyte, err = json.Marshal(app.Config) 102 if err != nil { 103 return nil, err 104 } 105 } 106 stmt, err := ds.db.Prepare("INSERT apps SET name=?,config=?") 107 108 if err != nil { 109 return nil, err 110 } 111 112 _, err = stmt.Exec(app.Name, string(cbyte)) 113 114 if err != nil { 115 mysqlErr := err.(*mysql.MySQLError) 116 if mysqlErr.Number == 1062 { 117 return nil, models.ErrAppsAlreadyExists 118 } 119 return nil, err 120 } 121 122 return app, nil 123 } 124 125 /* 126 UpdateApp updates an existing app on MySQL. 127 */ 128 func (ds *MySQLDatastore) UpdateApp(ctx context.Context, newapp *models.App) (*models.App, error) { 129 app := &models.App{Name: newapp.Name} 130 err := ds.Tx(func(tx *sql.Tx) error { 131 row := ds.db.QueryRow(`SELECT config FROM apps WHERE name=?`, app.Name) 132 133 var config string 134 if err := row.Scan(&config); err != nil { 135 if err == sql.ErrNoRows { 136 return models.ErrAppsNotFound 137 } 138 return err 139 } 140 141 if config != "" { 142 err := json.Unmarshal([]byte(config), &app.Config) 143 if err != nil { 144 return err 145 } 146 } 147 148 app.UpdateConfig(newapp.Config) 149 150 cbyte, err := json.Marshal(app.Config) 151 if err != nil { 152 return err 153 } 154 155 stmt, err := ds.db.Prepare(`UPDATE apps SET config=? WHERE name=?`) 156 157 if err != nil { 158 return err 159 } 160 161 res, err := stmt.Exec(string(cbyte), app.Name) 162 163 if err != nil { 164 return err 165 } 166 167 if n, err := res.RowsAffected(); err != nil { 168 return err 169 } else if n == 0 { 170 return models.ErrAppsNotFound 171 } 172 return nil 173 }) 174 175 if err != nil { 176 return nil, err 177 } 178 179 return app, nil 180 } 181 182 /* 183 RemoveApp removes an existing app on MySQL. 184 */ 185 func (ds *MySQLDatastore) RemoveApp(ctx context.Context, appName string) error { 186 _, err := ds.db.Exec(` 187 DELETE FROM apps 188 WHERE name = ? 189 `, appName) 190 191 if err != nil { 192 return err 193 } 194 195 return nil 196 } 197 198 /* 199 GetApp retrieves an app from MySQL. 200 */ 201 func (ds *MySQLDatastore) GetApp(ctx context.Context, name string) (*models.App, error) { 202 row := ds.db.QueryRow(`SELECT name, config FROM apps WHERE name=?`, name) 203 204 var resName string 205 var config string 206 err := row.Scan(&resName, &config) 207 208 res := &models.App{ 209 Name: resName, 210 } 211 212 json.Unmarshal([]byte(config), &res.Config) 213 214 if err != nil { 215 if err == sql.ErrNoRows { 216 return nil, models.ErrAppsNotFound 217 } 218 return nil, err 219 } 220 221 return res, nil 222 } 223 224 func scanApp(scanner rowScanner, app *models.App) error { 225 var configStr string 226 227 err := scanner.Scan( 228 &app.Name, 229 &configStr, 230 ) 231 232 json.Unmarshal([]byte(configStr), &app.Config) 233 234 return err 235 } 236 237 /* 238 GetApps retrieves an array of apps according to a specific filter. 239 */ 240 func (ds *MySQLDatastore) GetApps(ctx context.Context, filter *models.AppFilter) ([]*models.App, error) { 241 res := []*models.App{} 242 filterQuery, args := buildFilterAppQuery(filter) 243 rows, err := ds.db.Query(fmt.Sprintf("SELECT DISTINCT name, config FROM apps %s", filterQuery), args...) 244 if err != nil { 245 return nil, err 246 } 247 defer rows.Close() 248 249 for rows.Next() { 250 var app models.App 251 err := scanApp(rows, &app) 252 253 if err != nil { 254 break 255 } 256 res = append(res, &app) 257 } 258 259 if err := rows.Err(); err != nil { 260 return res, err 261 } 262 return res, nil 263 } 264 265 /* 266 InsertRoute inserts an route to MySQL. 267 */ 268 func (ds *MySQLDatastore) InsertRoute(ctx context.Context, route *models.Route) (*models.Route, error) { 269 hbyte, err := json.Marshal(route.Headers) 270 if err != nil { 271 return nil, err 272 } 273 274 cbyte, err := json.Marshal(route.Config) 275 if err != nil { 276 return nil, err 277 } 278 279 err = ds.Tx(func(tx *sql.Tx) error { 280 r := tx.QueryRow(`SELECT 1 FROM apps WHERE name=?`, route.AppName) 281 if err := r.Scan(new(int)); err != nil { 282 if err == sql.ErrNoRows { 283 return models.ErrAppsNotFound 284 } 285 } 286 same, err := tx.Query(`SELECT 1 FROM routes WHERE app_name=? AND path=?`, 287 route.AppName, route.Path) 288 if err != nil { 289 return err 290 } 291 defer same.Close() 292 if same.Next() { 293 return models.ErrRoutesAlreadyExists 294 } 295 296 _, err = tx.Exec(` 297 INSERT INTO routes ( 298 app_name, 299 path, 300 image, 301 format, 302 maxc, 303 memory, 304 type, 305 timeout, 306 idle_timeout, 307 headers, 308 config 309 ) 310 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`, 311 route.AppName, 312 route.Path, 313 route.Image, 314 route.Format, 315 route.MaxConcurrency, 316 route.Memory, 317 route.Type, 318 route.Timeout, 319 route.IdleTimeout, 320 string(hbyte), 321 string(cbyte), 322 ) 323 return err 324 }) 325 326 if err != nil { 327 return nil, err 328 } 329 return route, nil 330 } 331 332 /* 333 UpdateRoute updates an existing route on MySQL. 334 */ 335 func (ds *MySQLDatastore) UpdateRoute(ctx context.Context, newroute *models.Route) (*models.Route, error) { 336 var route models.Route 337 err := ds.Tx(func(tx *sql.Tx) error { 338 row := ds.db.QueryRow(fmt.Sprintf("%s WHERE app_name=? AND path=?", routeSelector), newroute.AppName, newroute.Path) 339 if err := scanRoute(row, &route); err == sql.ErrNoRows { 340 return models.ErrRoutesNotFound 341 } else if err != nil { 342 return err 343 } 344 345 route.Update(newroute) 346 347 hbyte, err := json.Marshal(route.Headers) 348 if err != nil { 349 return err 350 } 351 352 cbyte, err := json.Marshal(route.Config) 353 if err != nil { 354 return err 355 } 356 357 res, err := tx.Exec(` 358 UPDATE routes SET 359 image = ?, 360 format = ?, 361 maxc = ?, 362 memory = ?, 363 type = ?, 364 timeout = ?, 365 idle_timeout = ?, 366 headers = ?, 367 config = ? 368 WHERE app_name = ? AND path = ?;`, 369 route.Image, 370 route.Format, 371 route.MaxConcurrency, 372 route.Memory, 373 route.Type, 374 route.Timeout, 375 route.IdleTimeout, 376 string(hbyte), 377 string(cbyte), 378 route.AppName, 379 route.Path, 380 ) 381 382 if err != nil { 383 return err 384 } 385 386 if n, err := res.RowsAffected(); err != nil { 387 return err 388 } else if n == 0 { 389 return models.ErrRoutesNotFound 390 } 391 392 return nil 393 }) 394 395 if err != nil { 396 return nil, err 397 } 398 return &route, nil 399 } 400 401 /* 402 RemoveRoute removes an existing route on MySQL. 403 */ 404 func (ds *MySQLDatastore) RemoveRoute(ctx context.Context, appName, routePath string) error { 405 res, err := ds.db.Exec(` 406 DELETE FROM routes 407 WHERE path = ? AND app_name = ? 408 `, routePath, appName) 409 410 if err != nil { 411 return err 412 } 413 414 n, err := res.RowsAffected() 415 if err != nil { 416 return err 417 } 418 419 if n == 0 { 420 return models.ErrRoutesRemoving 421 } 422 423 return nil 424 } 425 426 func scanRoute(scanner rowScanner, route *models.Route) error { 427 var headerStr string 428 var configStr string 429 430 err := scanner.Scan( 431 &route.AppName, 432 &route.Path, 433 &route.Image, 434 &route.Format, 435 &route.MaxConcurrency, 436 &route.Memory, 437 &route.Type, 438 &route.Timeout, 439 &route.IdleTimeout, 440 &headerStr, 441 &configStr, 442 ) 443 if err != nil { 444 return err 445 } 446 447 if headerStr == "" { 448 return models.ErrRoutesNotFound 449 } 450 451 if err := json.Unmarshal([]byte(headerStr), &route.Headers); err != nil { 452 return err 453 } 454 return json.Unmarshal([]byte(configStr), &route.Config) 455 } 456 457 /* 458 GetRoute retrieves a route from MySQL. 459 */ 460 func (ds *MySQLDatastore) GetRoute(ctx context.Context, appName, routePath string) (*models.Route, error) { 461 var route models.Route 462 463 row := ds.db.QueryRow(fmt.Sprintf("%s WHERE app_name=? AND path=?", routeSelector), appName, routePath) 464 err := scanRoute(row, &route) 465 466 if err == sql.ErrNoRows { 467 return nil, models.ErrRoutesNotFound 468 } else if err != nil { 469 return nil, err 470 } 471 return &route, nil 472 } 473 474 /* 475 GetRoutes retrieves an array of routes according to a specific filter. 476 */ 477 func (ds *MySQLDatastore) GetRoutes(ctx context.Context, filter *models.RouteFilter) ([]*models.Route, error) { 478 res := []*models.Route{} 479 filterQuery, args := buildFilterRouteQuery(filter) 480 rows, err := ds.db.Query(fmt.Sprintf("%s %s", routeSelector, filterQuery), args...) 481 if err != nil { 482 return nil, err 483 } 484 defer rows.Close() 485 486 for rows.Next() { 487 var route models.Route 488 err := scanRoute(rows, &route) 489 if err != nil { 490 continue 491 } 492 res = append(res, &route) 493 494 } 495 if err := rows.Err(); err != nil { 496 return nil, err 497 } 498 return res, nil 499 } 500 501 /* 502 GetRoutesByApp retrieves a route with a specific app name. 503 */ 504 func (ds *MySQLDatastore) GetRoutesByApp(ctx context.Context, appName string, filter *models.RouteFilter) ([]*models.Route, error) { 505 res := []*models.Route{} 506 507 var filterQuery string 508 var args []interface{} 509 if filter == nil { 510 filterQuery = "WHERE app_name = ?" 511 args = []interface{}{appName} 512 } else { 513 filter.AppName = appName 514 filterQuery, args = buildFilterRouteQuery(filter) 515 } 516 rows, err := ds.db.Query(fmt.Sprintf("%s %s", routeSelector, filterQuery), args...) 517 if err != nil { 518 return nil, err 519 } 520 defer rows.Close() 521 522 for rows.Next() { 523 var route models.Route 524 err := scanRoute(rows, &route) 525 if err != nil { 526 continue 527 } 528 res = append(res, &route) 529 530 } 531 if err := rows.Err(); err != nil { 532 return nil, err 533 } 534 535 return res, nil 536 } 537 538 func buildFilterAppQuery(filter *models.AppFilter) (string, []interface{}) { 539 if filter == nil { 540 return "", nil 541 } 542 543 if filter.Name != "" { 544 return "WHERE name LIKE ?", []interface{}{filter.Name} 545 } 546 547 return "", nil 548 } 549 550 func buildFilterRouteQuery(filter *models.RouteFilter) (string, []interface{}) { 551 if filter == nil { 552 return "", nil 553 } 554 var b bytes.Buffer 555 var args []interface{} 556 557 where := func(colOp, val string) { 558 if val != "" { 559 args = append(args, val) 560 if len(args) == 1 { 561 fmt.Fprintf(&b, "WHERE %s ?", colOp) 562 } else { 563 fmt.Fprintf(&b, " AND %s ?", colOp) 564 } 565 } 566 } 567 568 where("path =", filter.Path) 569 where("app_name =", filter.AppName) 570 where("image =", filter.Image) 571 572 return b.String(), args 573 } 574 575 /* 576 Put inserts an extra into MySQL. 577 */ 578 func (ds *MySQLDatastore) Put(ctx context.Context, key, value []byte) error { 579 _, err := ds.db.Exec(` 580 INSERT INTO extras ( 581 id, 582 value 583 ) 584 VALUES (?, ?) 585 ON DUPLICATE KEY UPDATE 586 value = ? 587 `, string(key), string(value), string(value)) 588 589 if err != nil { 590 return err 591 } 592 593 return nil 594 } 595 596 /* 597 Get retrieves the value of a specific extra from MySQL. 598 */ 599 func (ds *MySQLDatastore) Get(ctx context.Context, key []byte) ([]byte, error) { 600 row := ds.db.QueryRow("SELECT value FROM extras WHERE id=?", key) 601 602 var value string 603 err := row.Scan(&value) 604 if err == sql.ErrNoRows { 605 return nil, nil 606 } else if err != nil { 607 return nil, err 608 } 609 610 return []byte(value), nil 611 } 612 613 /* 614 Tx Begins and commits a MySQL Transaction. 615 */ 616 func (ds *MySQLDatastore) Tx(f func(*sql.Tx) error) error { 617 tx, err := ds.db.Begin() 618 if err != nil { 619 return err 620 } 621 err = f(tx) 622 if err != nil { 623 tx.Rollback() 624 return err 625 } 626 return tx.Commit() 627 }