Skip to content

Commit

Permalink
client: Export public API.
Browse files Browse the repository at this point in the history
This patch, similar in spirit to #238, introduces a new public API for
extending the Pebble client. The basic idea behind this patch is the client mixin
struct embedded in commands' structs no longer holds an instance of the
Pebble-specific `client.Client` struct, but instead take a `ClientGetter` interface
that implements a `Client()` method. `ClientGetter.Client()` always returns a
Pebble-specific `client.Client` struct.

For applications that indent to extend Pebble, this means that either the Pebble
client or a new, application-specific client can be used. In the latter case,
the application-specific client must implement the `ClientGetter` interface so
that a Pebble-specific `client.Client` struct can always be derived by the
facilities consuming the `ClientGetter` interface. The easiest way to implement
this is to embed the Pebble client:

    type PebbleClient = client.Client
    type MyClient struct {
        *PebbleClient
    }

Since the Pebble-specific `client.Client` is embedded, and the `ClientGetter`
interface requires `Client()` to be implemented with a pointer receiver, the
snippet above suffices to implement a client based off the Pebble-supplied one
without much hassle, and that provides lower-level facilities for communicating
with the daemon, such as `DoSync()`, `DoAsync()` and `DoAsyncFull()`.
  • Loading branch information
anpep committed Jul 31, 2023
1 parent 00bcd1f commit 37e993b
Show file tree
Hide file tree
Showing 33 changed files with 222 additions and 111 deletions.
24 changes: 19 additions & 5 deletions client/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ type changeAndData struct {
// Change fetches information about a Change given its ID.
func (client *Client) Change(id string) (*Change, error) {
var chgd changeAndData
_, err := client.doSync("GET", "/v1/changes/"+id, nil, nil, nil, &chgd)
if err != nil {
if _, err := client.DoSync(&RequestInfo{
Method: "GET",
Path: "/v1/changes/" + id,
}, &chgd); err != nil {
return nil, err
}

Expand All @@ -111,7 +113,11 @@ func (client *Client) Abort(id string) (*Change, error) {
}

var chg Change
if _, err := client.doSync("POST", "/v1/changes/"+id, nil, nil, &body, &chg); err != nil {
if _, err := client.DoSync(&RequestInfo{
Method: "POST",
Path: "/v1/changes/" + id,
Body: &body,
}, &chg); err != nil {
return nil, err
}

Expand Down Expand Up @@ -158,7 +164,11 @@ func (client *Client) Changes(opts *ChangesOptions) ([]*Change, error) {
}

var chgds []changeAndData
_, err := client.doSync("GET", "/v1/changes", query, nil, nil, &chgds)
_, err := client.DoSync(&RequestInfo{
Method: "GET",
Path: "/v1/changes",
Query: query,
}, &chgds)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -190,7 +200,11 @@ func (client *Client) WaitChange(id string, opts *WaitChangeOptions) (*Change, e
query.Set("timeout", opts.Timeout.String())
}

_, err := client.doSync("GET", "/v1/changes/"+id+"/wait", query, nil, nil, &chgd)
_, err := client.DoSync(&RequestInfo{
Method: "GET",
Path: "/v1/changes/" + id + "/wait",
Query: query,
}, &chgd)
if err != nil {
return nil, err
}
Expand Down
6 changes: 5 additions & 1 deletion client/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ func (client *Client) Checks(opts *ChecksOptions) ([]*CheckInfo, error) {
query["names"] = opts.Names
}
var checks []*CheckInfo
_, err := client.doSync("GET", "/v1/checks", query, nil, nil, &checks)
_, err := client.DoSync(&RequestInfo{
Method: "GET",
Path: "/v1/checks",
Query: query,
}, &checks)
if err != nil {
return nil, err
}
Expand Down
74 changes: 57 additions & 17 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ type doer interface {
Do(*http.Request) (*http.Response, error)
}

// ClientGetter implementations must provide a way to convert themselves to
// Pebble client instances.
type ClientGetter interface {
// Client returns a Pebble client instance.
Client() *Client
}

type ClientSetter interface {
SetClient(ClientGetter)
}

// Config allows the user to customize client behavior.
type Config struct {
// BaseURL contains the base URL where the Pebble daemon is expected to be.
Expand Down Expand Up @@ -118,6 +129,10 @@ type Client struct {
getWebsocket getWebsocketFunc
}

func (c *Client) Client() *Client {
return c
}

type getWebsocketFunc func(url string) (clientWebsocket, error)

type clientWebsocket interface {
Expand Down Expand Up @@ -326,13 +341,27 @@ func decodeInto(reader io.Reader, v interface{}) error {
return nil
}

// doSync performs a request to the given path using the specified HTTP method.
// RequestInfo holds the information to perform a request to the daemon.
type RequestInfo struct {
Method string
Path string
Query url.Values
Headers map[string]string
Body io.Reader
}

// ResultInfo is empty for now, but this is the mechanism that conveys
// general information that makes sense to requests at a more general
// level, and might be disconnected from the specific request at hand.
type ResultInfo struct{}

// DoSync performs a request to the given path using the specified HTTP method.
// It expects a "sync" response from the API and on success decodes the JSON
// response payload into the given value using the "UseNumber" json decoding
// which produces json.Numbers instead of float64 types for numbers.
func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) {
func (client *Client) DoSync(req *RequestInfo, v interface{}) (*ResultInfo, error) {
var rsp response
if err := client.do(method, path, query, headers, body, &rsp); err != nil {
if err := client.do(req.Method, req.Path, req.Query, req.Headers, req.Body, &rsp); err != nil {
return nil, err
}
if err := rsp.err(client); err != nil {
Expand All @@ -354,22 +383,28 @@ func (client *Client) doSync(method, path string, query url.Values, headers map[
return &rsp.ResultInfo, nil
}

func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) {
_, changeID, err = client.doAsyncFull(method, path, query, headers, body)
// DoAsync performs a request to the given path using the specified HTTP method.
// It expects an "async" response from the API and on success returns the
// change ID.
func (client *Client) DoAsync(req *RequestInfo) (changeID string, err error) {
_, changeID, err = client.DoAsyncFull(req)
return
}

func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader) (result json.RawMessage, changeID string, err error) {
// DoAsync performs a request to the given path using the specified HTTP method.
// It expects an "async" response from the API and on success returns the raw
// JSON response from the daemon alongside the change ID.
func (client *Client) DoAsyncFull(req *RequestInfo) (result json.RawMessage, changeID string, err error) {
var rsp response

if err := client.do(method, path, query, headers, body, &rsp); err != nil {
if err := client.do(req.Method, req.Path, req.Query, req.Headers, req.Body, &rsp); err != nil {
return nil, "", err
}
if err := rsp.err(client); err != nil {
return nil, "", err
}
if rsp.Type != "async" {
return nil, "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type)
return nil, "", fmt.Errorf("expected async response for %q on %q, got %q", req.Method, req.Path, rsp.Type)
}
if rsp.StatusCode != 202 {
return nil, "", fmt.Errorf("operation not accepted")
Expand All @@ -381,11 +416,6 @@ func (client *Client) doAsyncFull(method, path string, query url.Values, headers
return rsp.Result, rsp.Change, nil
}

// ResultInfo is empty for now, but this is the mechanism that conveys
// general information that makes sense to requests at a more general
// level, and might be disconnected from the specific request at hand.
type ResultInfo struct{}

// A response produced by the REST API will usually fit in this
// (exceptions are the icons/ endpoints obvs)
type response struct {
Expand Down Expand Up @@ -476,7 +506,10 @@ type SysInfo struct {
func (client *Client) SysInfo() (*SysInfo, error) {
var sysInfo SysInfo

if _, err := client.doSync("GET", "/v1/system-info", nil, nil, nil, &sysInfo); err != nil {
if _, err := client.DoSync(&RequestInfo{
Method: "GET",
Path: "/v1/system-info",
}, &sysInfo); err != nil {
return nil, fmt.Errorf("cannot obtain system details: %w", err)
}

Expand All @@ -497,8 +530,11 @@ func (client *Client) DebugPost(action string, params interface{}, result interf
if err != nil {
return err
}

_, err = client.doSync("POST", "/v1/debug", nil, nil, bytes.NewReader(body), result)
_, err = client.DoSync(&RequestInfo{
Method: "POST",
Path: "/v1/debug",
Body: bytes.NewReader(body),
}, result)
return err
}

Expand All @@ -508,6 +544,10 @@ func (client *Client) DebugGet(action string, result interface{}, params map[str
for k, v := range params {
urlParams.Set(k, v)
}
_, err := client.doSync("GET", "/v1/debug", urlParams, nil, nil, &result)
_, err := client.DoSync(&RequestInfo{
Method: "GET",
Path: "/v1/debug",
Query: urlParams,
}, &result)
return err
}
7 changes: 6 additions & 1 deletion client/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,12 @@ func (client *Client) Exec(opts *ExecOptions) (*ExecProcess, error) {
headers := map[string]string{
"Content-Type": "application/json",
}
resultBytes, changeID, err := client.doAsyncFull("POST", "/v1/exec", nil, headers, &body)
resultBytes, changeID, err := client.DoAsyncFull(&RequestInfo{
Method: "POST",
Path: "/v1/exec",
Headers: headers,
Body: &body,
})
if err != nil {
return nil, err
}
Expand Down
5 changes: 4 additions & 1 deletion client/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ func (client *Client) Do(method, path string, query url.Values, body io.Reader,
}

func (client *Client) FakeAsyncRequest() (changeId string, err error) {
changeId, err = client.doAsync("GET", "/v1/async-test", nil, nil, nil)
changeId, err = client.DoAsync(&RequestInfo{
Method: "GET",
Path: "/v1/async-test",
})
if err != nil {
return "", fmt.Errorf("cannot do async test: %v", err)
}
Expand Down
20 changes: 17 additions & 3 deletions client/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ func (client *Client) ListFiles(opts *ListFilesOptions) ([]*FileInfo, error) {
}

var results []fileInfoResult
_, err := client.doSync("GET", "/v1/files", q, nil, nil, &results)
_, err := client.DoSync(&RequestInfo{
Method: "GET",
Path: "/v1/files",
Query: q,
}, &results)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -280,7 +284,12 @@ func (client *Client) MakeDir(opts *MakeDirOptions) error {
headers := map[string]string{
"Content-Type": "application/json",
}
if _, err := client.doSync("POST", "/v1/files", nil, headers, &body, &result); err != nil {
if _, err := client.DoSync(&RequestInfo{
Method: "POST",
Path: "/v1/files",
Headers: headers,
Body: &body,
}, &result); err != nil {
return err
}

Expand Down Expand Up @@ -347,7 +356,12 @@ func (client *Client) RemovePath(opts *RemovePathOptions) error {
headers := map[string]string{
"Content-Type": "application/json",
}
if _, err := client.doSync("POST", "/v1/files", nil, headers, &body, &result); err != nil {
if _, err := client.DoSync(&RequestInfo{
Method: "POST",
Path: "/v1/files",
Headers: headers,
Body: &body,
}, &result); err != nil {
return err
}

Expand Down
12 changes: 10 additions & 2 deletions client/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ func (client *Client) AddLayer(opts *AddLayerOptions) error {
if err := json.NewEncoder(&body).Encode(&payload); err != nil {
return err
}
_, err := client.doSync("POST", "/v1/layers", nil, nil, &body, nil)
_, err := client.DoSync(&RequestInfo{
Method: "POST",
Path: "/v1/layers",
Body: &body,
}, nil)
return err
}

Expand All @@ -64,7 +68,11 @@ func (client *Client) PlanBytes(_ *PlanOptions) (data []byte, err error) {
"format": []string{"yaml"},
}
var dataStr string
_, err = client.doSync("GET", "/v1/plan", query, nil, nil, &dataStr)
_, err = client.DoSync(&RequestInfo{
Method: "GET",
Path: "/v1/plan",
Query: query,
}, &dataStr)
if err != nil {
return nil, err
}
Expand Down
13 changes: 11 additions & 2 deletions client/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ func (client *Client) doMultiServiceAction(actionName string, services []string)
headers := map[string]string{
"Content-Type": "application/json",
}
return client.doAsyncFull("POST", "/v1/services", nil, headers, bytes.NewBuffer(data))
return client.DoAsyncFull(&RequestInfo{
Method: "POST",
Path: "/v1/services",
Headers: headers,
Body: bytes.NewBuffer(data),
})
}

type ServicesOptions struct {
Expand Down Expand Up @@ -119,7 +124,11 @@ func (client *Client) Services(opts *ServicesOptions) ([]*ServiceInfo, error) {
"names": []string{strings.Join(opts.Names, ",")},
}
var services []*ServiceInfo
_, err := client.doSync("GET", "/v1/services", query, nil, nil, &services)
_, err := client.DoSync(&RequestInfo{
Method: "GET",
Path: "/v1/services",
Query: query,
}, &services)
if err != nil {
return nil, err
}
Expand Down
6 changes: 5 additions & 1 deletion client/signals.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ func (client *Client) SendSignal(opts *SendSignalOptions) error {
if err != nil {
return fmt.Errorf("cannot encode JSON payload: %w", err)
}
_, err = client.doSync("POST", "/v1/signals", nil, nil, &body, nil)
_, err = client.DoSync(&RequestInfo{
Method: "POST",
Path: "/v1/signals",
Body: &body,
}, nil)
return err
}

Expand Down
12 changes: 10 additions & 2 deletions client/warnings.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ func (client *Client) Warnings(opts WarningsOptions) ([]*Warning, error) {
if opts.All {
q.Add("select", "all")
}
_, err := client.doSync("GET", "/v1/warnings", q, nil, nil, &jws)
_, err := client.DoSync(&RequestInfo{
Method: "GET",
Path: "/v1/warnings",
Query: q,
}, &jws)

ws := make([]*Warning, len(jws))
for i, jw := range jws {
Expand All @@ -77,6 +81,10 @@ func (client *Client) Okay(t time.Time) error {
if err := json.NewEncoder(&body).Encode(op); err != nil {
return err
}
_, err := client.doSync("POST", "/v1/warnings", nil, nil, &body, nil)
_, err := client.DoSync(&RequestInfo{
Method: "POST",
Path: "/v1/warnings",
Body: &body,
}, nil)
return err
}
Loading

0 comments on commit 37e993b

Please sign in to comment.