canonical / pebble

Take control of your internal daemons!
https://canonical-pebble.readthedocs-hosted.com/
GNU General Public License v3.0
143 stars 54 forks source link

feat(client): separate HTTP transport from client #307

Closed anpep closed 11 months ago

anpep commented 11 months ago

This PR exposes client connection information (such as the base URL, the HTTP request Doer and common HTTP utility functions for extended Pebble clients) under the Transport struct of new clientutil package.

This allows external applications to extend the Pebble client and perform HTTP requests to the underlying daemon without exposing the internal state of the client to third-party applications that use the client package to call Pebble APIs.

The clientutil package is not intended for these applications, but for software that extends Pebble.

The client API stays exactly the same, with the difference of Pebble and applications extending Pebble using new internal APIs to construct a client:

/* Example 1: Creating a client for extending the CLI. */
package main

import (
    "github.com/canonical/pebble/client"
    "github.com/canonical/pebble/internals/clientutil"
)

func main() {
    // Create a client transport instance (DoSync*)
    transport, err := clientutil.NewTransport(&clientutil.TransportOptions{
        Address: "https://localhost:3000",  // Either base URL or socket path.
        UserAgent: "MyApplication/0.1",
    })
    if err != nil { /* ... */ }

    // Create a Pebble client
    client := client.NewFromTransport(transport)

    // Pass in the client and the transport
    if err := cli.Run(client, transport); err != nil {
        /* ... */
    }
}

Pebble CLI will pass the client and transport through CmdOptions:

// CmdOptions exposes state made accessible during command execution.
type CmdOptions struct {
    Client    *client.Client
    Transport *clientutil.Transport
    Parser    *flags.Parser
}

Extending a Pebble client could be done in the following manner:

package xclient

import (
    "github.com/canonical/pebble/client"
    "github.com/canonical/pebble/internals/clientutil"
)

// Client is a Pebble-compatible gateway for talking to the X daemon.
// All APIs that Pebble supports are available, in addition to X-specific
// ones.
type Client struct {
    *client.Client
    transport *clientutil.Transport
}

func New(client *client.Client, transport *clientutil.Transport) *Client {
    return &Client{Client: client, transport: transport}
}

func (c *Client) Foo() {
        /* The Do(), DoSync(), DoAsync() APIs are prone to change -- WIP */ 
    c.transport.Do("POST", "/v1/foo", nil, nil, nil, nil)
}

And extended application's commands could be constructed the following way:

func init() {
    cli.AddCommand(&cli.CmdInfo{
        Name:        "foo",
        /* ... */
        New: func(opts *cli.CmdOptions) flags.Commander {
            return &cmdFoo{client: xclient.New(opts.Client, opts.Transport)}
        },
    })
}