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  }