-
Notifications
You must be signed in to change notification settings - Fork 260
Add NMAgent Client #1305
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 NMAgent Client #1305
Changes from all commits
3714fbc
e87cbe6
641388c
b9ef256
491171d
b9dcee3
0a3903a
e8a28c2
3278686
748680f
bf2f713
0119867
22f0fce
3a2993b
f2080ef
6bab759
e398a6e
ff5e8e9
be0e60c
7eb129d
a0b0fb0
50a007a
f073993
aeedeb9
f942f3b
005ca05
244fa4d
20fb63a
9c7191c
3910ae0
6f79a16
5cb39d6
d4f4d62
2bd7f8e
196e5c5
1fdf924
f5ac638
11ec25a
8774ca9
cecc8ba
d04614a
a9e1d24
dc9522a
ba69161
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| package nmagent | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "io" | ||
| "net" | ||
| "net/http" | ||
| "net/url" | ||
| "strconv" | ||
| "time" | ||
|
|
||
| "github.com/Azure/azure-container-networking/nmagent/internal" | ||
| "github.com/pkg/errors" | ||
| ) | ||
|
|
||
| // NewClient returns an initialized Client using the provided configuration. | ||
| func NewClient(c Config) (*Client, error) { | ||
| if err := c.Validate(); err != nil { | ||
| return nil, errors.Wrap(err, "validating config") | ||
| } | ||
|
|
||
| client := &Client{ | ||
| httpClient: &http.Client{ | ||
| Transport: &internal.WireserverTransport{ | ||
| Transport: http.DefaultTransport, | ||
| }, | ||
| }, | ||
| host: c.Host, | ||
| port: c.Port, | ||
| enableTLS: c.UseTLS, | ||
| retrier: internal.Retrier{ | ||
| // nolint:gomnd // the base parameter is explained in the function | ||
| Cooldown: internal.Exponential(1*time.Second, 2), | ||
| }, | ||
| } | ||
|
|
||
| return client, nil | ||
| } | ||
|
|
||
| // Client is an agent for exchanging information with NMAgent. | ||
| type Client struct { | ||
| httpClient *http.Client | ||
|
|
||
| // config | ||
| host string | ||
| port uint16 | ||
|
|
||
| enableTLS bool | ||
|
|
||
| retrier interface { | ||
| Do(context.Context, func() error) error | ||
| } | ||
| } | ||
|
|
||
| // JoinNetwork joins a node to a customer's virtual network. | ||
| func (c *Client) JoinNetwork(ctx context.Context, jnr JoinNetworkRequest) error { | ||
| req, err := c.buildRequest(ctx, jnr) | ||
| if err != nil { | ||
| return errors.Wrap(err, "building request") | ||
| } | ||
|
|
||
| err = c.retrier.Do(ctx, func() error { | ||
| resp, err := c.httpClient.Do(req) // nolint:govet // the shadow is intentional | ||
| if err != nil { | ||
| return errors.Wrap(err, "executing request") | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't seem like we read the body in some operations (maybe that's existing DNC behavior anyways). Would be nice to do something like
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The standard library will recycle connections if you close the body. I suspect that requiring package authors to drain the bodies was too obtuse, so they made The check to see whether an EOF was seen is still present, but it's short-circuited by
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Connection re-use (or lack of) can be shown with this example: package main
import (
"context"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptrace"
)
func main() {
clientTrace := &httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) {
log.Printf("conn: %+v", info)
},
}
ctx := httptrace.WithClientTrace(context.Background(), clientTrace)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", nil)
if err != nil {
log.Fatal(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
if _, err := io.Copy(ioutil.Discard, res.Body); err != nil {
log.Fatal(err)
}
res.Body.Close()
req, err = http.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", nil)
if err != nil {
log.Fatal(err)
}
_, err = http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
}You can comment out the code that consumes the body, and the trace should show no connection reuse. This is still the behavior in 1.18. It's possible that there are some scenarios where this won't be the case but this has bit me enough in the past to make me want to be extra careful :)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mind if I reuse this example as a repro for a Go issue? (I'll mention you on it) IMO, this violates least surprise, and it appears there's been some effort to make
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feel free. Code slightly modified from this article btw. Would be nice to see some clarification around the code you pointed to, though it seems to me that this behavior is still consistent with the docs. Maybe Gopher slack or golang-nuts would be a good place to ask first? |
||
| return die(resp.StatusCode, resp.Header, resp.Body) | ||
| } | ||
| return nil | ||
| }) | ||
|
|
||
| return err // nolint:wrapcheck // wrapping this just introduces noise | ||
| } | ||
|
|
||
| // GetNetworkConfiguration retrieves the configuration of a customer's virtual | ||
| // network. Only subnets which have been delegated will be returned. | ||
| func (c *Client) GetNetworkConfiguration(ctx context.Context, gncr GetNetworkConfigRequest) (VirtualNetwork, error) { | ||
| var out VirtualNetwork | ||
|
|
||
| req, err := c.buildRequest(ctx, gncr) | ||
| if err != nil { | ||
| return out, errors.Wrap(err, "building request") | ||
| } | ||
|
|
||
| err = c.retrier.Do(ctx, func() error { | ||
| resp, err := c.httpClient.Do(req) // nolint:govet // the shadow is intentional | ||
| if err != nil { | ||
| return errors.Wrap(err, "executing http request to") | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return die(resp.StatusCode, resp.Header, resp.Body) | ||
| } | ||
|
|
||
| ct := resp.Header.Get(internal.HeaderContentType) | ||
| if ct != internal.MimeJSON { | ||
| return NewContentError(ct, resp.Body, resp.ContentLength) | ||
| } | ||
|
|
||
| err = json.NewDecoder(resp.Body).Decode(&out) | ||
| if err != nil { | ||
| return errors.Wrap(err, "decoding json response") | ||
| } | ||
|
|
||
| return nil | ||
| }) | ||
|
|
||
| return out, err // nolint:wrapcheck // wrapping just introduces noise here | ||
| } | ||
|
|
||
| // PutNetworkContainer applies a Network Container goal state and publishes it | ||
| // to PubSub. | ||
| func (c *Client) PutNetworkContainer(ctx context.Context, pncr *PutNetworkContainerRequest) error { | ||
| req, err := c.buildRequest(ctx, pncr) | ||
| if err != nil { | ||
| return errors.Wrap(err, "building request") | ||
| } | ||
|
|
||
| resp, err := c.httpClient.Do(req) | ||
| if err != nil { | ||
| return errors.Wrap(err, "submitting request") | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return die(resp.StatusCode, resp.Header, resp.Body) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // DeleteNetworkContainer removes a Network Container, its associated IP | ||
| // addresses, and network policies from an interface. | ||
| func (c *Client) DeleteNetworkContainer(ctx context.Context, dcr DeleteContainerRequest) error { | ||
| req, err := c.buildRequest(ctx, dcr) | ||
| if err != nil { | ||
| return errors.Wrap(err, "building request") | ||
| } | ||
|
|
||
| resp, err := c.httpClient.Do(req) | ||
| if err != nil { | ||
| return errors.Wrap(err, "submitting request") | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return die(resp.StatusCode, resp.Header, resp.Body) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func die(code int, headers http.Header, body io.ReadCloser) error { | ||
| // nolint:errcheck // make a best effort to return whatever information we can | ||
| // returning an error here without the code and source would | ||
| // be less helpful | ||
| bodyContent, _ := io.ReadAll(body) | ||
| return Error{ | ||
| Code: code, | ||
| // this is a little strange, but the conversion below is to avoid forcing | ||
| // consumers to depend on an internal type (which they can't anyway) | ||
| Source: internal.GetErrorSource(headers).String(), | ||
| Body: bodyContent, | ||
| } | ||
| } | ||
|
|
||
| func (c *Client) hostPort() string { | ||
| port := strconv.Itoa(int(c.port)) | ||
| return net.JoinHostPort(c.host, port) | ||
| } | ||
|
|
||
| func (c *Client) buildRequest(ctx context.Context, req Request) (*http.Request, error) { | ||
| if err := req.Validate(); err != nil { | ||
| return nil, errors.Wrap(err, "validating request") | ||
| } | ||
|
|
||
| fullURL := &url.URL{ | ||
| Scheme: c.scheme(), | ||
| Host: c.hostPort(), | ||
| Path: req.Path(), | ||
| } | ||
|
|
||
| body, err := req.Body() | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "retrieving request body") | ||
| } | ||
|
|
||
| // nolint:wrapcheck // wrapping doesn't provide useful information | ||
| return http.NewRequestWithContext(ctx, req.Method(), fullURL.String(), body) | ||
| } | ||
|
|
||
| func (c *Client) scheme() string { | ||
| if c.enableTLS { | ||
| return "https" | ||
| } | ||
| return "http" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package nmagent | ||
|
|
||
| import ( | ||
| "net/http" | ||
|
|
||
| "github.com/Azure/azure-container-networking/nmagent/internal" | ||
| ) | ||
|
|
||
| // NewTestClient is a factory function available in tests only for creating | ||
| // NMAgent clients with a mock transport | ||
| func NewTestClient(transport http.RoundTripper) *Client { | ||
| return &Client{ | ||
| httpClient: &http.Client{ | ||
| Transport: &internal.WireserverTransport{ | ||
| Transport: transport, | ||
| }, | ||
| }, | ||
| host: "localhost", | ||
| port: 12345, | ||
| retrier: internal.Retrier{ | ||
| Cooldown: internal.AsFastAsPossible(), | ||
| }, | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.