Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pkger source driver support #377

Merged
merged 5 commits into from
Apr 21, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect
github.com/fsouza/fake-gcs-server v1.17.0
github.com/go-sql-driver/mysql v1.5.0
github.com/gobuffalo/here v0.6.0
github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang/snappy v0.0.1 // indirect
Expand All @@ -28,6 +29,7 @@ require (
github.com/jackc/pgconn v1.3.2 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/lib/pq v1.3.0
github.com/markbates/pkger v0.15.1
github.com/mattn/go-sqlite3 v1.10.0
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8
github.com/neo4j-drivers/gobolt v1.7.4 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gG
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
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/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI=
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo=
github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
Expand Down Expand Up @@ -226,6 +228,8 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/markbates/pkger v0.15.1 h1:3MPelV53RnGSW07izx5xGxl4e/sdRD6zqseIk0rMASY=
github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
Expand Down Expand Up @@ -522,6 +526,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
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=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
61 changes: 61 additions & 0 deletions source/pkger/pkger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package pkger

import (
"fmt"
"net/http"

"github.com/golang-migrate/migrate/v4/source"
"github.com/golang-migrate/migrate/v4/source/httpfs"
"github.com/markbates/pkger/pkging"
)

func init() {
source.Register("pkger", &driver{})
}

// Pkger is an implementation of http.FileSystem backed by an instance of
// pkging.Pkger.
type Pkger struct {
pkging.Pkger

// Path is the relative path location of the migrations. It is passed to
// httpfs.PartialDriver.Init. If unset "/" is used as all paths are
// absolute.
Path string
}

// Open implements http.FileSystem.
func (p *Pkger) Open(name string) (http.File, error) {
f, err := p.Pkger.Open(name)
if err != nil {
return nil, err
}
return f.(http.File), nil
}

type driver struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use a separate driver struct? You can embed httpfs.PartialDriver into Pkger

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the driver and the instance requires Open methods of different signatures.

  • source.Driver requires Open(url string) (source.Driver, error).
  • http.FileSystem requires Open(name string) (http.File, error).

It would've been nice if pkging.Pkger implemented http.FileSystem but it turns out in fact it does not. I might be missing something but I need to implement http.FileSystem to be able to pass it to the partial driver Init(fs http.FileSystem, path string) error method.

Pkger (previously Instance) wraps pkging.Pkger and implements http.FileSystem so that it can be passed to the partial driver.

driver implements the source.Driver interface by providing its Open method.

If this is all unclear I'm happy to rework it somehow but you will have to provide some guidance on what you think makes more sense.

I'm also curious to know if I'm just missing something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use one struct by not embedding pkging.Pkger.

e.g.

type Pkger struct {
    httpfs.PartialDriver
    pkger pkging.Pkger
}

It would've been nice if pkging.Pkger implemented http.FileSystem but it turns out in fact it does not.

Looks like it does

Pkger (previously Instance) wraps pkging.Pkger and implements http.FileSystem so that it can be passed to the partial driver.

You can pass Pkger.pkger to Init()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it does

The signatures are different. It does't return http.File but pkging.File. It's subtle but means

./pkger.go:56:28: cannot use instance.Pkger (type pkging.Pkger) as type http.FileSystem in argument to ds.PartialDriver.Init:
	pkging.Pkger does not implement http.FileSystem (wrong type for Open method)
		have Open(string) (pkging.File, error)
		want Open(string) (http.File, error)

What am I missing? I still don't see how it would work by simply not embedding pkging.Pkger.

If we want Pkger to be the driver and look like

type Pkger struct {
    httpfs.PartialDriver
    pkger pkging.Pkger
}

I'm not against that but I think we need to wrap Pkger.pkger to implement http.FileSystem. Another approach I can think of is to have

type fsFunc func(name string) (http.File, error)

func (f fsFunc) Open(name string) (http.File, error) {
  return f(name)
}

and then wrap pkging.Pkger just before calling Init like so

fs := fsFunc(func(name string) (http.File, error) {
	f, err := instance.pkger.Open(name)
	if err != nil {
		return nil, err
	}
	return f.(http.File), nil
})
if err := instace.Init(fs, instance.Path); err != nil {

What about that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this a bit more I'm honestly wondering if it's worth adding this driver at all since it essentially only wraps pkging.Pkger in http.FileSystem. It might make sense to just put it on the user to wrap it themselves and instead use httpfs.New directly. What do you think about that?

For packr.Box, the other packager, it should be even easier since it alreay implements http.FileSystem, so the user can pass it straight in.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signatures are different. It does't return http.File but pkging.File It's subtle but means ...

Oh wow, didn't notice that! That's annoying... Not sure why they went that route. At a quick glance, the interfaces look the same to me...
Looks like we'll need to wrap pkging.Pkger... I'm not picky about what type Pkger struct looks like as long as the fields aren't exported.

Thinking about this a bit more I'm honestly wondering if it's worth adding this driver at all since it essentially only wraps pkging.Pkger in http.FileSystem. It might make sense to just put it on the user to wrap it themselves and instead use httpfs.New directly. What do you think about that?

If you're only going to use migrate as a Go library, then httpfs.New() is probably the easiest and quickest route to get started. However, if you want to use DB URIs and the migrate CLI, you'll need to create a driver using the httpfs.PartialDriver and register it. You can't use httpfs.New() for this since doing so will hardcode the driver to the http.FileSystem specified via httpfs.New().

I'd recommend having CLI support to debug and fix any issues you have with your migrations

Copy link
Contributor Author

@hnnsgstfssn hnnsgstfssn Apr 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if you want to use DB URIs and the migrate CLI, you'll need to create a driver using the httpfs.PartialDriver and register it. You can't use httpfs.New() for this since doing so will hardcode the driver to the http.FileSystem specified via httpfs.New().

I'm not sure I understand what you mean by hardcoding the driver to the specified file sytem. Could you elaborate?

How would the CLI support a source that is explicitly for embedded data? What would it mean to tell the CLI to read migrations from pkger:///migrations?

Looking at go_bindata it doesn't implement CLI support and as you can see I just did the same by leaving source.Driver.Open unimplemented. Personally I only use migrate as a library.

That said, consumers of pkger mostly register resources in a global instance of pkging.Pkger using pkger.Apply and then access files using package scoped functions pkger.Open. This global instance it not, as far as I can tell, accessible. It seems like a good idea to use source.Driver.Open to return a driver that reads from the global pkging.Pkger instance. It would allow library users to opt for either WithInstance and their own instance of pkging.Pkger or for Open after registering migrations on the global instance with pkger.Apply. The pkger CLI currently only generates code that registers embedded resources in the global instance so it would be nice to allowing access to it.

I've actually already gone ahead ahead and implemented this along with increased test coverage. I'm happy to address any new feedback on that.

On the back of the above to partly answer my own question, pkger:///migrations will read migrations from the relative directory /migrations using the package scoped pkger.Open i.e. using a global instance of pkging.Pkger. However in the context of the CLI I'm not sure that helps anyone since they can't register migrations on the global instance that exists only in memory during the execution of the CLI, right?

httpfs.PartialDriver
}

// Open implements source.Driver. NOT IMPLEMENTED.
func (d *driver) Open(url string) (source.Driver, error) {
return nil, fmt.Errorf("not yet implemented")
}

// WithInstance returns a source.Driver that is backed by an instance of Pkger.
func WithInstance(instance *Pkger) (source.Driver, error) {
if instance.Path == "" {
instance.Path = "/"
}

var fs http.FileSystem
var ds driver

fs = instance

if err := ds.Init(fs, instance.Path); err != nil {
return nil, fmt.Errorf("failed to init: %w", err)
}

return &ds, nil
}
70 changes: 70 additions & 0 deletions source/pkger/pkger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package pkger

import (
"log"
"testing"

"github.com/gobuffalo/here"
st "github.com/golang-migrate/migrate/v4/source/testing"
"github.com/markbates/pkger/pkging"
"github.com/markbates/pkger/pkging/mem"
)

func Test(t *testing.T) {
i := testInstance(t)

d, err := WithInstance(i)
if err != nil {
t.Fatal(err)
}
st.Test(t, d)
}

func TestWithInstance(t *testing.T) {
i := testInstance(t)

_, err := WithInstance(i)
if err != nil {
t.Fatal(err)
}
}

func TestOpen(t *testing.T) {
d := &driver{}
_, err := d.Open("")
if err == nil {
t.Fatal("expected err, because it's not implemented yet")
}
}

func testInstance(t *testing.T) *Pkger {
info, err := here.New().Current()
if err != nil {
t.Fatalf("failed to get the current here.Info: %v\n", err)
}

pkg, err := mem.New(info)
if err != nil {
log.Fatalf("failed to create an in-memory pkging.Pkger: %v\n", err)
}

createMigrationFile(t, pkg, "/1_foobar.up.sql")
createMigrationFile(t, pkg, "/1_foobar.down.sql")
createMigrationFile(t, pkg, "/3_foobar.up.sql")
createMigrationFile(t, pkg, "/4_foobar.up.sql")
createMigrationFile(t, pkg, "/4_foobar.down.sql")
createMigrationFile(t, pkg, "/5_foobar.down.sql")
createMigrationFile(t, pkg, "/7_foobar.up.sql")
createMigrationFile(t, pkg, "/7_foobar.down.sql")

return &Pkger{
Pkger: pkg,
}
}

func createMigrationFile(t *testing.T, pkg pkging.Pkger, m string) {
_, err := pkg.Create(m)
if err != nil {
t.Fatalf("failed to package migration file %q: %v\n", m, err)
}
}