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

plugin: create plugin API and loader, add ipld-git plugin #4033

Merged
merged 23 commits into from
Jul 16, 2017
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7203c43
plugin: create plugin API and loader, add ipld-git plugin
whyrusleeping Jul 4, 2017
285a4d8
test: add TEST_NO_PLUGIN and PLUGIN prereq
Kubuxu Jul 12, 2017
d6f280e
make: fix megacheck being run on help print
Kubuxu Jul 12, 2017
d2cc708
make(sharness): add plugins as part of sharness build
Kubuxu Jul 12, 2017
2c71cf0
test: add git plugin test stub
Kubuxu Jul 12, 2017
04b26fe
make: fix buildmode=plugin and pkgdir problems
Kubuxu Jul 12, 2017
cfad460
git: make gitPlugin struct private
Kubuxu Jul 12, 2017
d6657ed
make codeclimate a bit more happy
Kubuxu Jul 12, 2017
f947931
git: add git plugin tests
magik6k Jul 12, 2017
42b0ba3
fixup plugin loading
Kubuxu Jul 12, 2017
b067972
make codeclimate more happy
Kubuxu Jul 13, 2017
c569b0b
plugin: make the plugin preload work again
Kubuxu Jul 13, 2017
e293de1
plugin: fix error when plugins dir does not exist
Kubuxu Jul 13, 2017
314d2c3
misc: make codecov really happy
Kubuxu Jul 13, 2017
be5ee0b
Disable ipldgit preload
Kubuxu Jul 13, 2017
b135a1f
misc: add go fmt to main packages of plugins
Kubuxu Jul 13, 2017
6da6e77
plugin: fix plugin loading
Kubuxu Jul 13, 2017
fb618a2
plugin: fix typos
Kubuxu Jul 14, 2017
36cbfa4
coverage: add dependency on DEPS_GO for coverage
Kubuxu Jul 14, 2017
2df6186
coverage: disable plugin tests during sharness coverage collection
Kubuxu Jul 14, 2017
b5ea00e
sharness: add debug output for TEST_NO_PLUGIN
Kubuxu Jul 14, 2017
3e8e039
fix typo
Kubuxu Jul 14, 2017
c0ee7d6
misc: small code style edit
Kubuxu Jul 16, 2017
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
7 changes: 6 additions & 1 deletion Rules.mk
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export IPFS_REUSEPORT=false
dir := bin
include $(dir)/Rules.mk

# tests need access to rules from plugin
dir := plugin
include $(dir)/Rules.mk

dir := test
include $(dir)/Rules.mk

Expand Down Expand Up @@ -56,6 +60,7 @@ include $(dir)/Rules.mk
dir := pin/internal/pb
include $(dir)/Rules.mk


# -------------------- #
# universal rules #
# -------------------- #
Expand Down Expand Up @@ -142,7 +147,7 @@ help:
@echo ' test_go_short'
@echo ' test_go_expensive'
@echo ' test_go_race'
@echo ' test_go_megacheck' - Run the `megacheck` vetting tool
@echo ' test_go_megacheck - Run the `megacheck` vetting tool'
@echo ' test_sharness_short'
@echo ' test_sharness_expensive'
@echo ' test_sharness_race'
Expand Down
7 changes: 7 additions & 0 deletions cmd/ipfs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/url"
"os"
"os/signal"
"path/filepath"
"runtime/pprof"
"strings"
"sync"
Expand All @@ -22,6 +23,7 @@ import (
cmdsHttp "github.com/ipfs/go-ipfs/commands/http"
core "github.com/ipfs/go-ipfs/core"
coreCmds "github.com/ipfs/go-ipfs/core/commands"
"github.com/ipfs/go-ipfs/plugin/loader"
repo "github.com/ipfs/go-ipfs/repo"
config "github.com/ipfs/go-ipfs/repo/config"
fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
Expand Down Expand Up @@ -339,6 +341,11 @@ func callCommand(ctx context.Context, req cmds.Request, root *cmds.Command, cmd
} else {
log.Debug("executing command locally")

pluginpath := filepath.Join(req.InvocContext().ConfigRoot, "plugins")
if _, err := loader.LoadPlugins(pluginpath); err != nil {
return nil, err
}

err := req.SetRootContext(ctx)
if err != nil {
return nil, err
Expand Down
36 changes: 14 additions & 22 deletions core/commands/dag/dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

cmds "github.com/ipfs/go-ipfs/commands"
coredag "github.com/ipfs/go-ipfs/core/coredag"
path "github.com/ipfs/go-ipfs/path"
pin "github.com/ipfs/go-ipfs/pin"

Expand Down Expand Up @@ -76,34 +77,25 @@ into an object of the specified format.
defer n.Blockstore.PinLock().Unlock()
}

var c *cid.Cid
switch ienc {
case "json":
nd, err := convertJsonToType(fi, format)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
nds, err := coredag.ParseInputs(ienc, format, fi)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

c, err = n.DAG.Add(nd)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
case "raw":
nd, err := convertRawToType(fi, format)
var c *cid.Cid
b := n.DAG.Batch()
for _, nd := range nds {
cid, err := b.Add(nd)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

c, err = n.DAG.Add(nd)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
default:
res.SetError(fmt.Errorf("unrecognized input encoding: %s", ienc), cmds.ErrNormal)
c = cid
}
if err := b.Commit(); err != nil {
res.SetError(err, cmds.ErrNormal)
return
}

Expand Down
91 changes: 91 additions & 0 deletions core/coredag/dagtransl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package coredag

import (
"fmt"
"io"
"io/ioutil"

node "gx/ipfs/QmYNyRZJBUYPNrLszFmrBrPJbsBh2vMsefz5gnDpB5M1P6/go-ipld-format"
ipldcbor "gx/ipfs/QmemYymP73eVdTUUMZEiSpiHeZQKNJdT5dP2iuHssZh1sR/go-ipld-cbor"
)

// DagParser is function used for parsing stream into Node
type DagParser func(r io.Reader) ([]node.Node, error)

// FormatParsers is used for mapping format descriptors to DagParsers
type FormatParsers map[string]DagParser

// InputEncParsers is used for mapping input encodings to FormatParsers
type InputEncParsers map[string]FormatParsers

// DefaultInputEncParsers is InputEncParser that is used everywhere
var DefaultInputEncParsers = InputEncParsers{
"json": defaultJSONParsers,
"raw": defaultRawParsers,
}
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure if making this global is the right decision.
This will make testing it harder.

Copy link
Member Author

Choose a reason for hiding this comment

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

If we dont use a global, registration becomes a much more complicated thing to do. Any ideas on a good way to do that?

Copy link
Member

Choose a reason for hiding this comment

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

Can't we do the same thing we're doing with BlockDecoders (pass it to register)?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I am planning that.

Copy link
Member

Choose a reason for hiding this comment

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

BlockDecoder is doing the same (it is using DefaultBlockDecoder from go-ipld-format).

Copy link
Member

Choose a reason for hiding this comment

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

Good point. The problem isn't really registration, it's using the the decoders (e.g. in ParseInputs).

Copy link
Member

Choose a reason for hiding this comment

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

In the Plugin API for IPLD I've sketched out I've made the registration map an argument so at least the plugins can be tested.

We have to figure out how to use the decoders well with non-global states.


var defaultJSONParsers = FormatParsers{
"cbor": cborJSONParser,
"dag-cbor": cborJSONParser,
}

var defaultRawParsers = FormatParsers{
"cbor": cborRawParser,
"dag-cbor": cborRawParser,
}

// ParseInputs uses DefaultInputEncParsers to parse io.Reader described by
// input encoding and format to an instance of ipld Node
func ParseInputs(ienc, format string, r io.Reader) ([]node.Node, error) {
return DefaultInputEncParsers.ParseInputs(ienc, format, r)
}

// AddParser adds DagParser under give input encoding and format
func (iep InputEncParsers) AddParser(ienv, format string, f DagParser) {
m, ok := iep[ienv]
if !ok {
m = make(FormatParsers)
iep[ienv] = m
}

m[format] = f
}

// ParseInputs parses io.Reader described by input encoding and format to
// an instance of ipld Node
func (iep InputEncParsers) ParseInputs(ienc, format string, r io.Reader) ([]node.Node, error) {
pset, ok := iep[ienc]
if !ok {
return nil, fmt.Errorf("no input parser for %q", ienc)
}

parser, ok := pset[format]
if !ok {
return nil, fmt.Errorf("no parser for format %q using input type %q", format, ienc)
}

return parser(r)
}

func cborJSONParser(r io.Reader) ([]node.Node, error) {
nd, err := ipldcbor.FromJson(r)
if err != nil {
return nil, err
}

return []node.Node{nd}, nil
}

func cborRawParser(r io.Reader) ([]node.Node, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}

nd, err := ipldcbor.Decode(data)
if err != nil {
return nil, err
}

return []node.Node{nd}, nil
}
3 changes: 2 additions & 1 deletion coverage/Rules.mk
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
include mk/header.mk

$(d)/coverage_deps:
$(d)/coverage_deps: $$(DEPS_GO)
rm -rf $(@D)/unitcover && mkdir $(@D)/unitcover
rm -rf $(@D)/sharnesscover && mkdir $(@D)/sharnesscover
ifneq ($(IPFS_SKIP_COVER_BINS),1)
Expand Down Expand Up @@ -41,6 +41,7 @@ endif

export IPFS_COVER_DIR:= $(realpath $(d))/sharnesscover/

$(d)/sharness_tests.coverprofile: export TEST_NO_PLUGIN=1
$(d)/sharness_tests.coverprofile: $(d)/ipfs cmd/ipfs/ipfs-test-cover $(d)/coverage_deps test_sharness_short
(cd $(@D)/sharnesscover && find . -type f | gocovmerge -list -) > $@

Expand Down
1 change: 1 addition & 0 deletions mk/util.mk
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# util functions
OS ?= $(shell sh -c 'uname -s 2>/dev/null || echo not')
ifeq ($(OS),Windows_NT)
WINDOWS :=1
?exe :=.exe # windows compat
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,12 @@
"hash": "QmPjTrrSfE6TzLv6ya6VWhGcCgPrUAdcgrDcQyRDX2VyW1",
"name": "go-libp2p-routing",
"version": "2.2.17"
},
{
"author": "whyrusleeping",
"hash": "Qma7Kuwun7w8SZphjEPDVxvGfetBkqdNGmigDA13sJdLex",
"name": "go-ipld-git",
"version": "0.1.3"
}
],
"gxVersion": "0.10.0",
Expand Down
9 changes: 9 additions & 0 deletions plugin/Rules.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
include mk/header.mk

dir := $(d)/loader
include $(dir)/Rules.mk

dir := $(d)/plugins
include $(dir)/Rules.mk

include mk/footer.mk
16 changes: 16 additions & 0 deletions plugin/ipld.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package plugin

import (
"github.com/ipfs/go-ipfs/core/coredag"

node "gx/ipfs/QmYNyRZJBUYPNrLszFmrBrPJbsBh2vMsefz5gnDpB5M1P6/go-ipld-format"
)

// PluginIPLD is an interface that can be implemented to add handlers for
// for different IPLD formats
type PluginIPLD interface {
Plugin

RegisterBlockDecoders(dec node.BlockDecoder) error
RegisterInputEncParsers(iec coredag.InputEncParsers) error
}
1 change: 1 addition & 0 deletions plugin/loader/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
preload.go
10 changes: 10 additions & 0 deletions plugin/loader/Rules.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
include mk/header.mk

$(d)/preload.go: d:=$(d)
$(d)/preload.go: $(d)/preload_list
$(d)/preload.sh > $@
go fmt $@ >/dev/null

DEPS_GO += $(d)/preload.go

include mk/footer.mk
45 changes: 45 additions & 0 deletions plugin/loader/initializer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package loader

import (
"github.com/ipfs/go-ipfs/core/coredag"
"github.com/ipfs/go-ipfs/plugin"

format "gx/ipfs/QmYNyRZJBUYPNrLszFmrBrPJbsBh2vMsefz5gnDpB5M1P6/go-ipld-format"
)

func initialize(plugins []plugin.Plugin) error {
for _, p := range plugins {
err := p.Init()
if err != nil {
return err
}
}

return nil
}

func run(plugins []plugin.Plugin) error {
for _, pl := range plugins {
err := runIPLDPlugin(pl)
if err != nil {
return err
}
}
return nil
}

func runIPLDPlugin(pl plugin.Plugin) error {
Copy link
Member Author

Choose a reason for hiding this comment

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

do a type switch up in the 'run' function, and have this function accept the proper type. Then we can error out if the types don't match correctly instead of silently ignoring the problem (which could easily happen with a mis-compiled plugin)

Copy link
Member

Choose a reason for hiding this comment

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

I was thinking about it, but a Plugin could in theory implement multiple interfaces, then typeswich isn't really helping us as it will choose just one interface (the first in the switch statement).

Miss-compiled plugin will error out or log if: the plugin lib can't be loaded, there is no Plugins variable, it has wrong type.

ipldpl, ok := pl.(plugin.PluginIPLD)
if !ok {
return nil
}

var err error
err = ipldpl.RegisterBlockDecoders(format.DefaultBlockDecoder)
Copy link
Member Author

Choose a reason for hiding this comment

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

why not just err := .... ?

if err != nil {
return err
}

err = ipldpl.RegisterInputEncParsers(coredag.DefaultInputEncParsers)
Copy link
Member Author

Choose a reason for hiding this comment

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

return ipldpl.Register....

return err
}
65 changes: 65 additions & 0 deletions plugin/loader/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package loader

import (
"fmt"
"os"

"github.com/ipfs/go-ipfs/plugin"

logging "gx/ipfs/QmSpJByNKFX1sCsHBEp3R73FL4NF6FnQTEGyNAXHm2GS52/go-log"
)

var log = logging.Logger("plugin/loader")

var loadPluginsFunc = func(string) ([]plugin.Plugin, error) {
return nil, nil
}

// LoadPlugins loads and initializes plugins.
func LoadPlugins(pluginDir string) ([]plugin.Plugin, error) {
plMap := make(map[string]plugin.Plugin)
for _, v := range preloadPlugins {
plMap[v.Name()] = v
}

newPls, err := loadDynamicPlugins(pluginDir)
if err != nil {
return nil, err
}

for _, pl := range newPls {
if ppl, ok := plMap[pl.Name()]; ok {
// plugin is already preloaded
return nil, fmt.Errorf(
"plugin: %s, is duplicated in version: %s, "+
"while trying to load dynamically: %s",
ppl.Name(), ppl.Version(), pl.Version())
}
plMap[pl.Name()] = pl
}

pls := make([]plugin.Plugin, 0, len(plMap))
for _, v := range plMap {
pls = append(pls, v)
}

err = initialize(pls)
if err != nil {
return nil, err
}

err = run(pls)
return nil, err
}

func loadDynamicPlugins(pluginDir string) ([]plugin.Plugin, error) {
_, err := os.Stat(pluginDir)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}

return loadPluginsFunc(pluginDir)
}
Loading