Fix #418 Added MySQL as DB storage layer. (#575)

* Fix #418 Added MySQL as DB storage layer.

* Make the mysql stuff work

* Make the mysql stuff work

* Make the mysql stuff work

* Make the mysql stuff work

* small fixes

* Switch to Go 1.8 installation inside CI (#589)

* Switch to Go 1.8 installation inside CI

Partially Addresses: #588

* Use url.Hostname() instead of custom method

* Added PR review changes.

* Added missing check for error.

* Changed * with name, config

* Removed unused import.

* Added check for NoRows

* Merged changes with HEAD

* Added documentation to mysql.go

* update mysql to be on par with postgres
This commit is contained in:
Martin Pinto-Bazurco Mendieta
2017-03-21 20:01:17 +01:00
committed by Seif Lotfy سيف لطفي
parent 353a144081
commit e4b3105d92
6 changed files with 757 additions and 17 deletions

View File

@@ -6,6 +6,7 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/iron-io/functions/api/datastore/bolt" "github.com/iron-io/functions/api/datastore/bolt"
"github.com/iron-io/functions/api/datastore/mysql"
"github.com/iron-io/functions/api/datastore/postgres" "github.com/iron-io/functions/api/datastore/postgres"
"github.com/iron-io/functions/api/datastore/redis" "github.com/iron-io/functions/api/datastore/redis"
"github.com/iron-io/functions/api/models" "github.com/iron-io/functions/api/models"
@@ -22,6 +23,8 @@ func New(dbURL string) (models.Datastore, error) {
return bolt.New(u) return bolt.New(u)
case "postgres": case "postgres":
return postgres.New(u) return postgres.New(u)
case "mysql":
return mysql.New(u)
case "redis": case "redis":
return redis.New(u) return redis.New(u)
default: default:

View File

@@ -8,12 +8,13 @@ import (
"github.com/iron-io/functions/api/models" "github.com/iron-io/functions/api/models"
"net/http"
"net/url"
"os"
"reflect"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"reflect"
"net/http"
"os"
"net/url"
) )
func setLogBuffer() *bytes.Buffer { func setLogBuffer() *bytes.Buffer {
@@ -73,12 +74,12 @@ func Test(t *testing.T, ds models.Datastore) {
{ {
// Set a config var // Set a config var
updated, err := ds.UpdateApp(ctx, updated, err := ds.UpdateApp(ctx,
&models.App{Name: testApp.Name, Config: map[string]string{"TEST":"1"}}) &models.App{Name: testApp.Name, Config: map[string]string{"TEST": "1"}})
if err != nil { if err != nil {
t.Log(buf.String()) t.Log(buf.String())
t.Fatalf("Test UpdateApp: error when updating app: %v", err) t.Fatalf("Test UpdateApp: error when updating app: %v", err)
} }
expected := &models.App{Name: testApp.Name, Config: map[string]string{"TEST":"1"}} expected := &models.App{Name: testApp.Name, Config: map[string]string{"TEST": "1"}}
if !reflect.DeepEqual(*updated, *expected) { if !reflect.DeepEqual(*updated, *expected) {
t.Log(buf.String()) t.Log(buf.String())
t.Fatalf("Test UpdateApp: expected updated `%v` but got `%v`", expected, updated) t.Fatalf("Test UpdateApp: expected updated `%v` but got `%v`", expected, updated)
@@ -86,12 +87,12 @@ func Test(t *testing.T, ds models.Datastore) {
// Set a different var (without clearing the existing) // Set a different var (without clearing the existing)
updated, err = ds.UpdateApp(ctx, updated, err = ds.UpdateApp(ctx,
&models.App{Name: testApp.Name, Config: map[string]string{"OTHER":"TEST"}}) &models.App{Name: testApp.Name, Config: map[string]string{"OTHER": "TEST"}})
if err != nil { if err != nil {
t.Log(buf.String()) t.Log(buf.String())
t.Fatalf("Test UpdateApp: error when updating app: %v", err) t.Fatalf("Test UpdateApp: error when updating app: %v", err)
} }
expected = &models.App{Name: testApp.Name, Config: map[string]string{"TEST":"1","OTHER":"TEST"}} expected = &models.App{Name: testApp.Name, Config: map[string]string{"TEST": "1", "OTHER": "TEST"}}
if !reflect.DeepEqual(*updated, *expected) { if !reflect.DeepEqual(*updated, *expected) {
t.Log(buf.String()) t.Log(buf.String())
t.Fatalf("Test UpdateApp: expected updated `%v` but got `%v`", expected, updated) t.Fatalf("Test UpdateApp: expected updated `%v` but got `%v`", expected, updated)
@@ -99,12 +100,12 @@ func Test(t *testing.T, ds models.Datastore) {
// Delete a var // Delete a var
updated, err = ds.UpdateApp(ctx, updated, err = ds.UpdateApp(ctx,
&models.App{Name: testApp.Name, Config: map[string]string{"TEST":""}}) &models.App{Name: testApp.Name, Config: map[string]string{"TEST": ""}})
if err != nil { if err != nil {
t.Log(buf.String()) t.Log(buf.String())
t.Fatalf("Test UpdateApp: error when updating app: %v", err) t.Fatalf("Test UpdateApp: error when updating app: %v", err)
} }
expected = &models.App{Name: testApp.Name, Config: map[string]string{"OTHER":"TEST"}} expected = &models.App{Name: testApp.Name, Config: map[string]string{"OTHER": "TEST"}}
if !reflect.DeepEqual(*updated, *expected) { if !reflect.DeepEqual(*updated, *expected) {
t.Log(buf.String()) t.Log(buf.String())
t.Fatalf("Test UpdateApp: expected updated `%v` but got `%v`", expected, updated) t.Fatalf("Test UpdateApp: expected updated `%v` but got `%v`", expected, updated)
@@ -247,7 +248,6 @@ func Test(t *testing.T, ds models.Datastore) {
} }
} }
// Testing update // Testing update
{ {
// Update some fields, and add 3 configs and 3 headers. // Update some fields, and add 3 configs and 3 headers.
@@ -485,4 +485,4 @@ var testRoute = &models.Route{
Image: "iron/hello", Image: "iron/hello",
Type: "sync", Type: "sync",
Format: "http", Format: "http",
} }

View File

@@ -0,0 +1,621 @@
package mysql
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"net/url"
"github.com/Sirupsen/logrus"
"github.com/go-sql-driver/mysql"
_ "github.com/go-sql-driver/mysql"
"github.com/iron-io/functions/api/datastore/internal/datastoreutil"
"github.com/iron-io/functions/api/models"
)
const routesTableCreate = `CREATE TABLE IF NOT EXISTS routes (
app_name varchar(256) NOT NULL,
path varchar(256) NOT NULL,
image varchar(256) NOT NULL,
format varchar(16) NOT NULL,
maxc int NOT NULL,
memory int NOT NULL,
timeout int NOT NULL,
type varchar(16) NOT NULL,
headers text NOT NULL,
config text NOT NULL,
PRIMARY KEY (app_name, path)
);`
const appsTableCreate = `CREATE TABLE IF NOT EXISTS apps (
name varchar(256) NOT NULL PRIMARY KEY,
config text NOT NULL
);`
const extrasTableCreate = `CREATE TABLE IF NOT EXISTS extras (
id varchar(256) NOT NULL PRIMARY KEY,
value varchar(256) NOT NULL
);`
const routeSelector = `SELECT app_name, path, image, format, maxc, memory, type, timeout, headers, config FROM routes`
type rowScanner interface {
Scan(dest ...interface{}) error
}
type rowQuerier interface {
QueryRow(query string, args ...interface{}) *sql.Row
}
/*
MySQLDatastore defines a basic MySQL Datastore struct.
*/
type MySQLDatastore struct {
db *sql.DB
}
/*
New creates a new MySQL Datastore.
*/
func New(url *url.URL) (models.Datastore, error) {
u := fmt.Sprintf("%s@%s%s", url.User.String(), url.Host, url.Path)
db, err := sql.Open("mysql", u)
if err != nil {
return nil, err
}
err = db.Ping()
if err != nil {
return nil, err
}
maxIdleConns := 30
db.SetMaxIdleConns(maxIdleConns)
logrus.WithFields(logrus.Fields{"max_idle_connections": maxIdleConns}).Info("MySQL dialed")
pg := &MySQLDatastore{
db: db,
}
for _, v := range []string{routesTableCreate, appsTableCreate, extrasTableCreate} {
_, err = db.Exec(v)
if err != nil {
return nil, err
}
}
return datastoreutil.NewValidator(pg), nil
}
/*
InsertApp inserts an app to MySQL.
*/
func (ds *MySQLDatastore) InsertApp(ctx context.Context, app *models.App) (*models.App, error) {
var cbyte []byte
var err error
if app.Config != nil {
cbyte, err = json.Marshal(app.Config)
if err != nil {
return nil, err
}
}
stmt, err := ds.db.Prepare("INSERT apps SET name=?,config=?")
if err != nil {
return nil, err
}
_, err = stmt.Exec(app.Name, string(cbyte))
if err != nil {
mysqlErr := err.(*mysql.MySQLError)
if mysqlErr.Number == 1062 {
return nil, models.ErrAppsAlreadyExists
}
return nil, err
}
return app, nil
}
/*
UpdateApp updates an existing app on MySQL.
*/
func (ds *MySQLDatastore) UpdateApp(ctx context.Context, newapp *models.App) (*models.App, error) {
app := &models.App{Name: newapp.Name}
err := ds.Tx(func(tx *sql.Tx) error {
row := ds.db.QueryRow(`SELECT config FROM apps WHERE name=?`, app.Name)
var config string
if err := row.Scan(&config); err != nil {
if err == sql.ErrNoRows {
return models.ErrAppsNotFound
}
return err
}
if config != "" {
err := json.Unmarshal([]byte(config), &app.Config)
if err != nil {
return err
}
}
app.UpdateConfig(newapp.Config)
cbyte, err := json.Marshal(app.Config)
if err != nil {
return err
}
stmt, err := ds.db.Prepare(`UPDATE apps SET config=? WHERE name=?`)
if err != nil {
return err
}
res, err := stmt.Exec(string(cbyte), app.Name)
if err != nil {
return err
}
if n, err := res.RowsAffected(); err != nil {
return err
} else if n == 0 {
return models.ErrAppsNotFound
}
return nil
})
if err != nil {
return nil, err
}
return app, nil
}
/*
RemoveApp removes an existing app on MySQL.
*/
func (ds *MySQLDatastore) RemoveApp(ctx context.Context, appName string) error {
_, err := ds.db.Exec(`
DELETE FROM apps
WHERE name = ?
`, appName)
if err != nil {
return err
}
return nil
}
/*
GetApp retrieves an app from MySQL.
*/
func (ds *MySQLDatastore) GetApp(ctx context.Context, name string) (*models.App, error) {
row := ds.db.QueryRow(`SELECT name, config FROM apps WHERE name=?`, name)
var resName string
var config string
err := row.Scan(&resName, &config)
res := &models.App{
Name: resName,
}
json.Unmarshal([]byte(config), &res.Config)
if err != nil {
if err == sql.ErrNoRows {
return nil, models.ErrAppsNotFound
}
return nil, err
}
return res, nil
}
func scanApp(scanner rowScanner, app *models.App) error {
var configStr string
err := scanner.Scan(
&app.Name,
&configStr,
)
json.Unmarshal([]byte(configStr), &app.Config)
return err
}
/*
GetApps retrieves an array of apps according to a specific filter.
*/
func (ds *MySQLDatastore) GetApps(ctx context.Context, filter *models.AppFilter) ([]*models.App, error) {
res := []*models.App{}
filterQuery, args := buildFilterAppQuery(filter)
rows, err := ds.db.Query(fmt.Sprintf("SELECT DISTINCT name, config FROM apps %s", filterQuery), args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var app models.App
err := scanApp(rows, &app)
if err != nil {
break
}
res = append(res, &app)
}
if err := rows.Err(); err != nil {
return res, err
}
return res, nil
}
/*
InsertRoute inserts an route to MySQL.
*/
func (ds *MySQLDatastore) InsertRoute(ctx context.Context, route *models.Route) (*models.Route, error) {
hbyte, err := json.Marshal(route.Headers)
if err != nil {
return nil, err
}
cbyte, err := json.Marshal(route.Config)
if err != nil {
return nil, err
}
err = ds.Tx(func(tx *sql.Tx) error {
r := tx.QueryRow(`SELECT 1 FROM apps WHERE name=?`, route.AppName)
if err := r.Scan(new(int)); err != nil {
if err == sql.ErrNoRows {
return models.ErrAppsNotFound
}
}
same, err := tx.Query(`SELECT 1 FROM routes WHERE app_name=? AND path=?`,
route.AppName, route.Path)
if err != nil {
return err
}
defer same.Close()
if same.Next() {
return models.ErrRoutesAlreadyExists
}
_, err = tx.Exec(`
INSERT INTO routes (
app_name,
path,
image,
format,
maxc,
memory,
type,
timeout,
headers,
config
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`,
route.AppName,
route.Path,
route.Image,
route.Format,
route.MaxConcurrency,
route.Memory,
route.Type,
route.Timeout,
string(hbyte),
string(cbyte),
)
return err
})
if err != nil {
return nil, err
}
return route, nil
}
/*
UpdateRoute updates an existing route on MySQL.
*/
func (ds *MySQLDatastore) UpdateRoute(ctx context.Context, newroute *models.Route) (*models.Route, error) {
var route models.Route
err := ds.Tx(func(tx *sql.Tx) error {
row := ds.db.QueryRow(fmt.Sprintf("%s WHERE app_name=? AND path=?", routeSelector), newroute.AppName, newroute.Path)
if err := scanRoute(row, &route); err == sql.ErrNoRows {
return models.ErrRoutesNotFound
} else if err != nil {
return err
}
route.Update(newroute)
hbyte, err := json.Marshal(route.Headers)
if err != nil {
return err
}
cbyte, err := json.Marshal(route.Config)
if err != nil {
return err
}
res, err := tx.Exec(`
UPDATE routes SET
image = ?,
format = ?,
maxc = ?,
memory = ?,
type = ?,
timeout = ?,
headers = ?,
config = ?
WHERE app_name = ? AND path = ?;`,
route.Image,
route.Format,
route.MaxConcurrency,
route.Memory,
route.Type,
route.Timeout,
string(hbyte),
string(cbyte),
route.AppName,
route.Path,
)
if err != nil {
return err
}
if n, err := res.RowsAffected(); err != nil {
return err
} else if n == 0 {
return models.ErrRoutesNotFound
}
return nil
})
if err != nil {
return nil, err
}
return &route, nil
}
/*
RemoveRoute removes an existing route on MySQL.
*/
func (ds *MySQLDatastore) RemoveRoute(ctx context.Context, appName, routePath string) error {
res, err := ds.db.Exec(`
DELETE FROM routes
WHERE path = ? AND app_name = ?
`, routePath, appName)
if err != nil {
return err
}
n, err := res.RowsAffected()
if err != nil {
return err
}
if n == 0 {
return models.ErrRoutesRemoving
}
return nil
}
func scanRoute(scanner rowScanner, route *models.Route) error {
var headerStr string
var configStr string
err := scanner.Scan(
&route.AppName,
&route.Path,
&route.Image,
&route.Format,
&route.MaxConcurrency,
&route.Memory,
&route.Type,
&route.Timeout,
&headerStr,
&configStr,
)
if err != nil {
return err
}
if headerStr == "" {
return models.ErrRoutesNotFound
}
if err := json.Unmarshal([]byte(headerStr), &route.Headers); err != nil {
return err
}
return json.Unmarshal([]byte(configStr), &route.Config)
}
/*
GetRoute retrieves a route from MySQL.
*/
func (ds *MySQLDatastore) GetRoute(ctx context.Context, appName, routePath string) (*models.Route, error) {
var route models.Route
row := ds.db.QueryRow(fmt.Sprintf("%s WHERE app_name=? AND path=?", routeSelector), appName, routePath)
err := scanRoute(row, &route)
if err == sql.ErrNoRows {
return nil, models.ErrRoutesNotFound
} else if err != nil {
return nil, err
}
return &route, nil
}
/*
GetRoutes retrieves an array of routes according to a specific filter.
*/
func (ds *MySQLDatastore) GetRoutes(ctx context.Context, filter *models.RouteFilter) ([]*models.Route, error) {
res := []*models.Route{}
filterQuery, args := buildFilterRouteQuery(filter)
rows, err := ds.db.Query(fmt.Sprintf("%s %s", routeSelector, filterQuery), args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var route models.Route
err := scanRoute(rows, &route)
if err != nil {
continue
}
res = append(res, &route)
}
if err := rows.Err(); err != nil {
return nil, err
}
return res, nil
}
/*
GetRoutesByApp retrieves a route with a specific app name.
*/
func (ds *MySQLDatastore) GetRoutesByApp(ctx context.Context, appName string, filter *models.RouteFilter) ([]*models.Route, error) {
res := []*models.Route{}
var filterQuery string
var args []interface{}
if filter == nil {
filterQuery = "WHERE app_name = ?"
args = []interface{}{appName}
} else {
filter.AppName = appName
filterQuery, args = buildFilterRouteQuery(filter)
}
rows, err := ds.db.Query(fmt.Sprintf("%s %s", routeSelector, filterQuery), args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var route models.Route
err := scanRoute(rows, &route)
if err != nil {
continue
}
res = append(res, &route)
}
if err := rows.Err(); err != nil {
return nil, err
}
return res, nil
}
func buildFilterAppQuery(filter *models.AppFilter) (string, []interface{}) {
if filter == nil {
return "", nil
}
if filter.Name != "" {
return "WHERE name LIKE ?", []interface{}{filter.Name}
}
return "", nil
}
func buildFilterRouteQuery(filter *models.RouteFilter) (string, []interface{}) {
if filter == nil {
return "", nil
}
var b bytes.Buffer
var args []interface{}
where := func(colOp, val string) {
if val != "" {
args = append(args, val)
if len(args) == 1 {
fmt.Fprintf(&b, "WHERE %s ?", colOp)
} else {
fmt.Fprintf(&b, " AND %s ?", colOp)
}
}
}
where("path =", filter.Path)
where("app_name =", filter.AppName)
where("image =", filter.Image)
return b.String(), args
}
/*
Put inserts an extra into MySQL.
*/
func (ds *MySQLDatastore) Put(ctx context.Context, key, value []byte) error {
_, err := ds.db.Exec(`
INSERT INTO extras (
id,
value
)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE
value = ?
`, string(key), string(value), string(value))
if err != nil {
return err
}
return nil
}
/*
Get retrieves the value of a specific extra from MySQL.
*/
func (ds *MySQLDatastore) Get(ctx context.Context, key []byte) ([]byte, error) {
row := ds.db.QueryRow("SELECT value FROM extras WHERE id=?", key)
var value string
err := row.Scan(&value)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, err
}
return []byte(value), nil
}
/*
Tx Begins and commits a MySQL Transaction.
*/
func (ds *MySQLDatastore) Tx(f func(*sql.Tx) error) error {
tx, err := ds.db.Begin()
if err != nil {
return err
}
err = f(tx)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}

View File

@@ -0,0 +1,112 @@
package mysql
import (
"bytes"
"database/sql"
"fmt"
"net/url"
"os/exec"
"testing"
"time"
"github.com/iron-io/functions/api/datastore/internal/datastoretest"
)
const tmpMysql = "mysql://root:root@tcp(%v:3307)/funcs"
func prepareMysqlTest(logf, fatalf func(string, ...interface{})) (func(), func()) {
fmt.Println("initializing mysql for test")
tryRun(logf, "remove old mysql container", exec.Command("docker", "rm", "-f", "iron-mysql-test"))
mustRun(fatalf, "start mysql container", exec.Command(
"docker", "run", "--name", "iron-mysql-test", "-p", "3307:3306", "-e", "MYSQL_DATABASE=funcs",
"-e", "MYSQL_ROOT_PASSWORD=root", "-d", "mysql"))
maxWait := 16 * time.Second
wait := 2 * time.Second
var db *sql.DB
var err error
for {
db, err = sql.Open("mysql", fmt.Sprintf("root:root@tcp(%v:3307)/",
datastoretest.GetContainerHostIP()))
if err != nil {
if wait > maxWait {
fatalf("failed to connect to mysql after %d seconds", maxWait)
break
}
fmt.Println("failed to connect to mysql:", err)
fmt.Println("retrying in:", wait)
time.Sleep(wait)
continue
}
// Ping
if _, err = db.Exec("SELECT 1"); err != nil {
fmt.Println("failed to ping database:", err)
time.Sleep(wait)
continue
}
break
}
_, err = db.Exec("DROP DATABASE IF EXISTS funcs;")
if err != nil {
fmt.Println("failed to drop database:", err)
}
_, err = db.Exec("CREATE DATABASE funcs;")
if err != nil {
fatalf("failed to create database: %s\n", err)
}
_, err = db.Exec(`GRANT ALL PRIVILEGES ON funcs.* TO root@localhost WITH GRANT OPTION;`)
if err != nil {
fatalf("failed to grant priviledges to user 'mysql: %s\n", err)
panic(err)
}
fmt.Println("mysql for test ready")
return func() {
db, err := sql.Open("mysql", fmt.Sprintf("root:root@tcp(%v:3307)/",
datastoretest.GetContainerHostIP()))
if err != nil {
fatalf("failed to connect for truncation: %s\n", err)
}
for _, table := range []string{"routes", "apps", "extras"} {
_, err = db.Exec(`TRUNCATE TABLE ` + table)
if err != nil {
fatalf("failed to truncate table %q: %s\n", table, err)
}
}
},
func() {
tryRun(logf, "stop mysql container", exec.Command("docker", "rm", "-f", "iron-mysql-test"))
}
}
func TestDatastore(t *testing.T) {
_, close := prepareMysqlTest(t.Logf, t.Fatalf)
defer close()
u, err := url.Parse(fmt.Sprintf(tmpMysql, datastoretest.GetContainerHostIP()))
if err != nil {
t.Fatalf("failed to parse url: %s\n", err)
}
ds, err := New(u)
if err != nil {
t.Fatalf("failed to create mysql datastore: %s\n", err)
}
datastoretest.Test(t, ds)
}
func tryRun(logf func(string, ...interface{}), desc string, cmd *exec.Cmd) {
var b bytes.Buffer
cmd.Stderr = &b
if err := cmd.Run(); err != nil {
logf("failed to %s: %s", desc, b.String())
}
}
func mustRun(fatalf func(string, ...interface{}), desc string, cmd *exec.Cmd) {
var b bytes.Buffer
cmd.Stderr = &b
if err := cmd.Run(); err != nil {
fatalf("failed to %s: %s", desc, b.String())
}
}

12
glide.lock generated
View File

@@ -1,5 +1,5 @@
hash: 01f9cff01b9ee5c1d8c37c86779ab6bbd91278394e526f921f399f10a0523698 hash: c1cb358ca30836b70eedffdf0132cf9b9d0da981dbb9d58fc2f75eabe91de429
updated: 2017-02-21T08:50:25.925523311-08:00 updated: 2017-03-11T12:22:28.171415703+01:00
imports: imports:
- name: github.com/amir/raidman - name: github.com/amir/raidman
version: c74861fe6a7bb8ede0a010ce4485bdbb4fc4c985 version: c74861fe6a7bb8ede0a010ce4485bdbb4fc4c985
@@ -107,7 +107,7 @@ imports:
- name: github.com/fsnotify/fsnotify - name: github.com/fsnotify/fsnotify
version: fd9ec7deca8bf46ecd2a795baaacf2b3a9be1197 version: fd9ec7deca8bf46ecd2a795baaacf2b3a9be1197
- name: github.com/fsouza/go-dockerclient - name: github.com/fsouza/go-dockerclient
version: 364c822d280c4f34afc3339e50d4fc0129d6b5ec version: fbeb72ccd29aa2596f364a5a85af622c651c3e16
- name: github.com/garyburd/redigo - name: github.com/garyburd/redigo
version: 0708def8b0cf3a05acdf44a7f28b864c2958921d version: 0708def8b0cf3a05acdf44a7f28b864c2958921d
subpackages: subpackages:
@@ -151,6 +151,8 @@ imports:
version: 027696d4b54399770f1cdcc6c6daa56975f9e14e version: 027696d4b54399770f1cdcc6c6daa56975f9e14e
- name: github.com/go-resty/resty - name: github.com/go-resty/resty
version: ef723efa2a1b4fcdbafb5b1e7c6cf42065519728 version: ef723efa2a1b4fcdbafb5b1e7c6cf42065519728
- name: github.com/go-sql-driver/mysql
version: a0583e0143b1624142adab07e0e97fe106d99561
- name: github.com/golang/groupcache - name: github.com/golang/groupcache
version: 72d04f9fcdec7d3821820cc4a6f150eae553639a version: 72d04f9fcdec7d3821820cc4a6f150eae553639a
subpackages: subpackages:
@@ -184,7 +186,7 @@ imports:
- json/scanner - json/scanner
- json/token - json/token
- name: github.com/heroku/docker-registry-client - name: github.com/heroku/docker-registry-client
version: 95467b6cacee2a06f112a3cf7e47a70fad6000cf version: 36bd5f538a6b9e70f2d863c9a8f6bf955a98eddc
subpackages: subpackages:
- registry - registry
- name: github.com/iron-io/functions_go - name: github.com/iron-io/functions_go
@@ -263,7 +265,7 @@ imports:
- name: github.com/satori/go.uuid - name: github.com/satori/go.uuid
version: 879c5887cd475cd7864858769793b2ceb0d44feb version: 879c5887cd475cd7864858769793b2ceb0d44feb
- name: github.com/Sirupsen/logrus - name: github.com/Sirupsen/logrus
version: c078b1e43f58d563c74cebe63c85789e76ddb627 version: 0208149b40d863d2c1a2f8fe5753096a9cf2cc8b
subpackages: subpackages:
- hooks/syslog - hooks/syslog
- name: github.com/spf13/afero - name: github.com/spf13/afero

View File

@@ -36,3 +36,5 @@ import:
- package: github.com/golang/groupcache - package: github.com/golang/groupcache
subpackages: subpackages:
- consistenthash - consistenthash
- package: github.com/go-sql-driver/mysql
version: ~1.3.0