diff --git a/.golangci.yml b/.golangci.yml index 69b4b15..ecd117e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,4 +6,4 @@ run: linters-settings: errcheck: - ignore: fmt:.*,github.com/go-kit/kit/log:.*,github.com/boltdb/bolt:.*,github.com/tchaudhry91/spinme/spin:^Slash.* + ignore: fmt:.*,github.com/go-kit/kit/log:.*,github.com/boltdb/bolt:.*,github.com/tchaudhry91/spinme/spin:^Slash.*,go.mongodb.org/mongo-driver/mongo:^Disconnect.* diff --git a/README.md b/README.md index c200dbf..18bd2d9 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,60 @@ A simple wrapper around Docker to quickly run "spin" up supporting containers such as databases for development. SpinMe can be invoked via the CLI binary or used as a library straight from your Go code. +## Library + +The primary goal of this library is to eliminate the need to externally spin up testing databases (esp while running tests on a CI system). +You can create live docker containers straight from your go test files, making `go test` independent. This allows you to test against real databases, without having to mock anything. + +See examples for Postgres/MySQL/Mongo/Redis in the [GoDoc](https://godoc.org/github.com/tchaudhry91/spinme/spin) + +Sample Usage: +``` +package main + +import ( + "context" + "database/sql" + "fmt" + "time" + + _ "github.com/go-sql-driver/mysql" + "github.com/tchaudhry91/spinme/spin" +) + +func main() { + out, err := spin.MySQL(context.Background(), nil) + if err != nil { + fmt.Println(err) + return + } + defer spin.SlashID(context.Background(), out.ID) + // Give mysql a minute to boot-up, sadly there is no "ready" check yet + time.Sleep(1 * time.Minute) + connStr, err := spin.MySQLConnString(out) + if err != nil { + fmt.Println(err) + return + } + db, err := sql.Open("mysql", connStr) + if err != nil { + fmt.Println(err) + return + } + defer db.Close() + err = db.Ping() + if err != nil { + fmt.Println(err) + return + } +} +``` + ## CLI Use this for daily use to start/stop/view your containers. -Usage: +### CLI Usage: ``` spinme -h SpinMe is a wrapper around docker to run common applications. @@ -48,13 +97,6 @@ The password wherever needed is set to `password`. Once the container is up, you e.g `--env PG_PASSWORD=1231` -## Library - -This is a very early release, the eventual goal is to be able to use this system with CIs, to allow stuff like `go test` to create containers for databases on the fly and clean them up. - -[GoDoc](https://godoc.org/github.com/tchaudhry91/spinme/spin) - - ## Contributing Contributions are very welcome. Please see create an issue! diff --git a/go.mod b/go.mod index e0490b3..e38182c 100644 --- a/go.mod +++ b/go.mod @@ -12,18 +12,26 @@ require ( github.com/docker/docker v0.7.3-0.20190731001754-589f1dad8dad github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.4.0 // indirect + github.com/go-redis/redis v6.15.5+incompatible github.com/go-sql-driver/mysql v1.4.1 + github.com/go-stack/stack v1.8.0 // indirect github.com/gogo/protobuf v1.2.1 // indirect github.com/golang/protobuf v1.3.1 // indirect + github.com/golang/snappy v0.0.1 // indirect github.com/gorilla/mux v1.7.3 // indirect github.com/lib/pq v1.2.0 github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect + github.com/onsi/ginkgo v1.10.1 // indirect + github.com/onsi/gomega v1.7.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.8.1 github.com/spf13/cobra v0.0.5 github.com/stretchr/testify v1.3.0 // indirect + github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect + github.com/xdg/stringprep v1.0.0 // indirect + go.mongodb.org/mongo-driver v1.1.0 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect google.golang.org/grpc v1.22.1 // indirect diff --git a/go.sum b/go.sum index 4d2a27b..84b119e 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,12 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-redis/redis v6.15.5+incompatible h1:pLky8I0rgiblWfa8C1EV7fPEUv0aH6vKRaYHc/YRHVk= +github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -37,12 +41,16 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -59,6 +67,11 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= @@ -91,16 +104,27 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.mongodb.org/mongo-driver v1.1.0 h1:aeOqSrhl9eDRAap/3T5pCfMBEBxZ0vuXBP+RMtp2KX8= +go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= @@ -118,6 +142,11 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/grpc v1.22.1 h1:/7cs52RnTJmD43s3uxzlq2U7nqVTd/37viQwMrMNlOM= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/spin/databases.go b/spin/databases.go index d2e30c0..12e36f6 100644 --- a/spin/databases.go +++ b/spin/databases.go @@ -35,6 +35,19 @@ func Mongo(ctx context.Context, c *SpinConfig) (SpinOut, error) { return o, err } +// MongoConnString returns the connection string from the spun mongo container +func MongoConnString(out SpinOut) (connStr string, err error) { + var hostEp string + var ok bool + + // Grab the host endpoint mapping for the container + if hostEp, ok = out.Endpoints["27017/tcp"]; !ok { + return "", errors.New("Failed to find proper port binding") + } + connStr = fmt.Sprintf("mongodb://%s:%s@%s", LookupEnv("MONGO_INITDB_ROOT_USERNAME", out.Env), LookupEnv("MONGO_INITDB_ROOT_PASSWORD", out.Env), hostEp) + return connStr, nil +} + // MySQL spins a MySQL container with the given settings. Nil config uses defaults func MySQL(ctx context.Context, c *SpinConfig) (SpinOut, error) { if c == nil { @@ -72,7 +85,7 @@ func MySQLConnString(out SpinOut) (connStr string, err error) { if hostEp, ok = out.Endpoints["3306/tcp"]; !ok { return "", errors.New("Failed to find proper port binding") } - connStr = fmt.Sprintf("root:%s@tcp(%s)/%s", lookupEnv("MYSQL_ROOT_PASSWORD", out.Env), hostEp, lookupEnv("MYSQL_DATABASE", out.Env)) + connStr = fmt.Sprintf("root:%s@tcp(%s)/%s", LookupEnv("MYSQL_ROOT_PASSWORD", out.Env), hostEp, LookupEnv("MYSQL_DATABASE", out.Env)) return connStr, nil } @@ -115,7 +128,7 @@ func PostgresConnString(out SpinOut) (connStr string, err error) { } // pq needs an independent port, not the entire endpoint ep := strings.Split(hostEp, ":") - connStr = fmt.Sprintf("user=postgres password=%s dbname=%s port=%s sslmode=disable", lookupEnv("POSTGRES_PASSWORD", out.Env), lookupEnv("POSTGRES_DB", out.Env), ep[len(ep)-1]) + connStr = fmt.Sprintf("user=postgres password=%s dbname=%s port=%s sslmode=disable", LookupEnv("POSTGRES_PASSWORD", out.Env), LookupEnv("POSTGRES_DB", out.Env), ep[len(ep)-1]) return connStr, nil } @@ -140,3 +153,15 @@ func Redis(ctx context.Context, c *SpinConfig) (SpinOut, error) { o.Service = "redis" return o, err } + +// RedisConnString returns the address of the redis container +func RedisConnString(out SpinOut) (connString string, err error) { + var hostEp string + var ok bool + + // Grab the host endpoint mapping for the container + if hostEp, ok = out.Endpoints["6379/tcp"]; !ok { + return "", errors.New("Failed to find proper port binding") + } + return hostEp, nil +} diff --git a/spin/databases_test.go b/spin/databases_test.go index 11024f7..6042c7a 100644 --- a/spin/databases_test.go +++ b/spin/databases_test.go @@ -10,6 +10,11 @@ import ( _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + + "github.com/go-redis/redis" ) func ExamplePostgres() { @@ -69,3 +74,64 @@ func ExampleMySQL() { fmt.Println("Connected!") //Output: Connected! } + +func ExampleMongo() { + out, err := spin.Mongo(context.Background(), nil) + if err != nil { + fmt.Println(err) + return + } + defer spin.SlashID(context.Background(), out.ID) + // Give mongo a few seconds to boot-up, sadly there is no "ready" check yet + time.Sleep(10 * time.Second) + connStr, err := spin.MongoConnString(out) + if err != nil { + fmt.Println(err) + return + } + client, err := mongo.NewClient(options.Client().ApplyURI(connStr)) + if err != nil { + fmt.Println(err) + return + } + err = client.Connect(context.Background()) + if err != nil { + fmt.Println(err) + return + } + defer client.Disconnect(context.Background()) + err = client.Ping(context.Background(), nil) + if err != nil { + fmt.Println(err) + return + } + fmt.Println("Connected!") + // Output: Connected! +} + +func ExampleRedis() { + out, err := spin.Redis(context.Background(), nil) + if err != nil { + fmt.Println(err) + return + } + defer spin.SlashID(context.Background(), out.ID) + // Give redis a few seconds to boot-up, sadly there is no "ready" check yet + time.Sleep(10 * time.Second) + connStr, err := spin.RedisConnString(out) + if err != nil { + fmt.Println(err) + return + } + + client := redis.NewClient(&redis.Options{ + Addr: connStr, + }) + pong, err := client.Ping().Result() + if err != nil { + fmt.Println(err) + return + } + fmt.Println(pong) + // Output: PONG +} diff --git a/spin/spin.go b/spin/spin.go index c915b85..53072d4 100644 --- a/spin/spin.go +++ b/spin/spin.go @@ -159,8 +159,8 @@ func buildName(svc string) string { return fmt.Sprintf("spinme-%s-%d", svc, time.Now().Unix()) } -// lookupEnv returns the value (blank for not found) in the containers environment -func lookupEnv(key string, env []string) string { +// LookupEnv returns the value (blank for not found) in the containers environment +func LookupEnv(key string, env []string) string { for _, e := range env { esplit := strings.Split(e, "=") if len(esplit) < 2 {