-
Notifications
You must be signed in to change notification settings - Fork 295
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
Feat #305: Libp2p support for REST API #349
Conversation
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
NewCluster() now takes an optional Host parameter. The rationale is to allow to re-use an existing libp2p Host when creating the cluster. The NewClusterHost method now allows to create a host with the options used by cluster. License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
This adds support for parameters to create a libp2p host in the REST API configuration: ID, PrivateKey and ListenMultiaddr. These parameters default to nil/empty and are ommited in the default configuration. They are only supposed to be used when the user wants the REST API to use a different libp2p host than a provided one (upcoming changes). Pnet protector not supported yet in this case. Underlying basic auth should cover that front. Will implement if someone has a usecase. License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
df8efb3
to
822402f
Compare
822402f
to
b5d2bb8
Compare
This commits allows restapi to serve/tunnel http on a libp2p stream. NewWitHost(...) allows to provide a libp2p host during initialization which is then used to obtain a listener with go-libp2p-gostream. Alternatively, if the configuration provides an ID, PrivateKey and Libp2pListenAddr, a host is created directly by us and used to get the listener. The protocol tag used is provided by the p2phttp library which will be used by the client. All tests now run against the libp2p node too. License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
This adds support for libp2p-tunneled http to the rest api component. If PeerAddr is specified in the configuration, then we will create a libp2p host and communicate with the API using that. Tests run now in both http and libp2p mode. Note: pnet support not included, but coming up License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
Also, re-use some convinient functions provided by pnet. License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
…that case. License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
b5d2bb8
to
73581b0
Compare
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
So they can serve as multi-module helpers without having circular deps. License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
Should reduce complexity (codeclimate). License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome @hsanjuan. A few minor nitpicks and questions. Otherwise, LGTM.
api/rest/client/client.go
Outdated
func (c *Client) setupHostname() error { | ||
// When no host/port/multiaddress defined, we set the default | ||
if c.config.APIAddr == nil && c.config.Host == "" && c.config.Port == "" { | ||
c.config.APIAddr, _ = ma.NewMultiaddr(DefaultAPIAddr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not return the error from NewMultiaddr
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not feeling super guilty to panic when anyone sets DefaultAPIAddr to something stupid, but ok :)
api/rest/client/client.go
Outdated
resolveCtx, cancel := context.WithTimeout(c.ctx, c.config.Timeout) | ||
defer cancel() | ||
resolved, err := madns.Resolve(resolveCtx, c.config.APIAddr) | ||
c.config.APIAddr = resolved[0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error check for madns.Resolve
? If madns.Resolve
fails, the slice access will panic
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ops, yeah
@@ -19,7 +19,7 @@ func (c *Client) do(method, path string, body io.Reader, obj interface{}) error | |||
} | |||
|
|||
func (c *Client) doRequest(method, path string, body io.Reader) (*http.Response, error) { | |||
urlpath := c.urlPrefix + "/" + strings.TrimPrefix(path, "/") | |||
urlpath := c.net + "://" + c.hostname + "/" + strings.TrimPrefix(path, "/") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've considered it. It doesn't add much (other than lines of code). I'll keep it in mind if we ever need to escape query params in more than one place.
api/rest/client/transports.go
Outdated
return err | ||
} | ||
|
||
// This should resolve addr too. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify, is this a TODO
comment or h.Peerstore().AddAddr()
resolves the address when adding it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. I thought it did, but it doesn't.
@@ -97,23 +122,19 @@ func (cfg *Config) Default() error { | |||
// Validate makes sure that all fields in this Config have | |||
// working values, at least in appearance. | |||
func (cfg *Config) Validate() error { | |||
if cfg.ListenAddr == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these if
statements can be placed in a switch
statement that has no expression, i.e.
switch {
case cfg.ReadTimeout < 0:
return errors.New("restapi.read_timeout is invalid")
case cfg.ReadHeaderTimeout < 0:
return errors.New("restapi.read_header_timeout is invalid")
<etc>
}
See go wiki.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, and look better
api/rest/config.go
Outdated
@@ -202,13 +300,28 @@ func (cfg *Config) ToJSON() (raw []byte, err error) { | |||
}() | |||
|
|||
jcfg := &jsonConfig{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason to not move the field initialisations inside the struct initialisation?
jcfg := &jsonConfig{
HTTPListenMultiaddress: cfg.HTTPListenAddr.String(),
<etc>
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
marginally clearer to me when not all the fields in the object are initialized within the constructor. But ok.
api/rest/restapi.go
Outdated
return nil | ||
} | ||
|
||
// Make new host. Override existing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we providing a host.Host
to NewAPIWithHost
if it gets overridden regardless?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API component is passed in the main cluster component's host (see 488 in ipfs-cluster-service/main.go). If the api config doesn't specify an independent address for a new host to listen on then the api will reuse the same host. This was a bit hard to follow and though there is some documentation in the comment on line 198, perhaps we could add this to the docs somewhere. In particular would be good to remind users that even if they don't specify a libp2p listen addr in their api config they will still have libp2p listening enabled by default. Perhaps libp2p listening on the default host should be turned off or on by a flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed, only gets overriden when there is configuration for a custom host. So configuration overrides provided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ZenGround0 it doesn't hurt to have libp2p listening enabled by default with the cluster host. Notice that the custom-host options are hidden by default in default configs, meaning, I only want expert users (or those who really read the godocs) to use custom-host. Until we revamp the config, I think this is going to be more or less undocumented.
api/util.go
Outdated
// encapsulates a new /ipfs/<peerID> address. | ||
func Libp2pMultiaddrJoin(addr ma.Multiaddr, p peer.ID) ma.Multiaddr { | ||
pidAddr, err := ma.NewMultiaddr("/ipfs/" + peer.IDB58Encode(p)) | ||
// let this break badly |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename function to MustLibp2pMultiaddrJoin
to convey that it will panic if it fails?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
config/util.go
Outdated
Name string | ||
} | ||
|
||
// ParseDurations takes a time.Duration src and saves it to the given dst. into the given |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove leftover comment on the end of the line, into the given
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
ipfs-cluster-ctl/main.go
Outdated
configured with the --host option. | ||
configured with the --host option. To use the secure libp2p-http | ||
API endpoint, use "--host" with the full cluster libp2p listener | ||
address (including the "/ipfs/<peerID>" part), and --secret. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be a bit clearer that --secret
is referring to the CLUSTER_SECRET
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's in the help for the flag, but added explanation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still working through this, here are my comments halfway through commit 6.
@@ -56,9 +56,9 @@ | |||
"version": "2.3.6" | |||
}, | |||
{ | |||
"author": "whyrusleeping", | |||
"author": "hsanjuan", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this repo published somewhere now? I looked under your repos as you are the new author but couldn't find it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/gxed/go4-lock
I'm the packager, that's why my name is there. Honestly, it's difficult to figure out the true source of something with gx.
cluster.go
Outdated
return nil, err | ||
|
||
if host == nil { | ||
h, err := NewClusterHost(ctx, cfg) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there something wrong with host, err = NewClusterHost(ctx, cfg)
? It's a small change but would reduce noise a little bit if this is ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
@@ -1097,71 +1095,6 @@ func (c *Cluster) Peers() []api.ID { | |||
return peers | |||
} | |||
|
|||
// makeHost makes a libp2p-host. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great to see this going away
clusterhost.go
Outdated
ma "github.com/multiformats/go-multiaddr" | ||
) | ||
|
||
// NewClusterHost creates a libp2p Host with the options in the from the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "in the from the"
clusterhost.go
Outdated
} | ||
|
||
// EncodeProtectorKey converts a byte slice to its hex string representation. | ||
func EncodeProtectorKey(secretBytes []byte) string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As this is simply a wrapper around hex.EncodeToString I am uncertain that a separate function is warranted for this.
api/rest/restapi.go
Outdated
} | ||
|
||
func (api *API) setupLibp2p(ctx context.Context) error { | ||
if api.config.Libp2pListenAddr == nil && api.host == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this check for api.host seems gratuitous as the same check happens on line 175 and the code between here and there can execute without problem regardless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right
api/rest/restapi.go
Outdated
return nil | ||
} | ||
|
||
// Make new host. Override existing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API component is passed in the main cluster component's host (see 488 in ipfs-cluster-service/main.go). If the api config doesn't specify an independent address for a new host to listen on then the api will reuse the same host. This was a bit hard to follow and though there is some documentation in the comment on line 198, perhaps we could add this to the docs somewhere. In particular would be good to remind users that even if they don't specify a libp2p listen addr in their api config they will still have libp2p listening enabled by default. Perhaps libp2p listening on the default host should be turned off or on by a flag.
func NewAPI(cfg *Config) (*API, error) { | ||
err := cfg.Validate() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't get called anymore when creating a new api component and I'm guessing it still should.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you' re right!
api/rest/restapi.go
Outdated
for _, a := range api.host.Addrs() { | ||
logger.Infof(" - %s/ipfs/%s", a, api.host.ID().Pretty()) | ||
} | ||
// TODO(h): Do we need a different server? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this resolved?
@@ -317,6 +434,9 @@ func (api *API) Shutdown() error { | |||
// requests. | |||
func (api *API) SetClient(c *rpc.Client) { | |||
api.rpcClient = c | |||
|
|||
// One notification for http server and one for libp2p server. | |||
api.rpcReady <- struct{}{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will block indefinitely in the case where the api starts up with only an http server or only a libp2p server
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, I increased channel capacity to two elements.
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
all comments addressed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a comment typo.
api/rest/restapi.go
Outdated
|
||
// Make new host. Override existing | ||
// Make new host. Override any provided existing one | ||
// iif we have config for a custom one, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: iif
and stray ,
at the end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn' t make much sense anyways
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
iff is fine, iif is not 👍
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More comments
apiMAddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") | ||
h, err := libp2p.New(context.Background(), libp2p.ListenAddrs(apiMAddr)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it ok for libp2p and http to listen on the same network address?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Port 0 means it will get assigned a random port so, not the same address in the end.
httpResp, err := c.Do(req) | ||
processResp(t, httpResp, err, resp) | ||
} | ||
|
||
type testF func(t *testing.T, url urlF) | ||
|
||
func testBothEndpoints(t *testing.T, test testF) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function makes me really happy
} | ||
|
||
func TestAPIPeerRemoveEndpoint(t *testing.T) { | ||
rest := testAPI(t) | ||
defer rest.Shutdown() | ||
|
||
makeDelete(t, apiURL(rest)+"/peers/"+test.TestPeerID1.Pretty(), &struct{}{}) | ||
tf := func(t *testing.T, url urlF) { | ||
makeDelete(t, rest, url(rest)+"/peers/"+test.TestPeerID1.Pretty(), &struct{}{}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if there is a better way for the test to ensure that the mock endpoint corresponding to Delete on /peers was called as a result of making this request. Right now I don't know if anything will visibly fail if this doesn't happen. I realize this doesn't pertain to this PR but the previous test code and so maybe a fix doesn't belong here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If hitting an unexistent endpoint (or it returns an error) processResp
will fail, I think
api/rest/client/client.go
Outdated
if cfg.Timeout == 0 { | ||
cfg.Timeout = DefaultTimeout | ||
c.client = &http.Client{ | ||
Transport: tr, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not set c.transport to tr? What else would the transport field be used for.
) | ||
|
||
func testClients(t *testing.T, api *rest.API, f func(*testing.T, *Client)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why don't these run in parallel?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if that makes you happy :)
ipfs-cluster-ctl/main.go
Outdated
@@ -81,7 +82,12 @@ func main() { | |||
cli.StringFlag{ | |||
Name: "host, l", | |||
Value: defaultHost, | |||
Usage: "multiaddress of the IPFS Cluster service API", | |||
Usage: "multiaddress of the IPFS Cluster REST API HTTP or LibP2P endpoint", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now this is a little misleading because both libp2p + default endpoints serve HTTP.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any ideas? I'm not sure how to explain this in a 120 char line.. (it's better explained above).
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more small thing and then I am happy with this.
api/rest/config.go
Outdated
@@ -209,23 +213,9 @@ func (cfg *Config) loadHTTPOptions(jcfg *jsonConfig) error { | |||
key := jcfg.SSLKeyFile | |||
cfg.pathSSLCertFile = cert | |||
cfg.pathSSLKeyFile = key |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like these lines can be deleted as cfg.tlsOptions()
sets cfg.pathSSLCertFile
and cfg.pathSSLKeyFile
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
This does something very cool:
Since the cluster peer has a libp2p host, we can pipe the http traffic under encrypted libp2p streams and basically get encrypted traffic to/from the API for free, without needing to setup certificates.
Doing this has required a bunch of changes:
--host
parameter is an libp2p multiaddress (<...>/ipfs/peer_id), and configure the client accordingly.The rest: expand the tests to use both plain and tunneled http, deal with PNet, use new libp2p host constructors, use new Pnet helpers and other cosmetics.
This is a very big PR, but the commits in correspond to each step, are self-contained to components and the log messages explain the change, so probably easy to check one by one.
Note, there will still be some refactoring/function splitting coming (I want to leave a green codeclimate).
Fixes #305 .