mirror of https://github.com/k3s-io/k3s
191 lines
4.4 KiB
Go
191 lines
4.4 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/rootless-containers/rootlesskit/pkg/api"
|
|
"github.com/rootless-containers/rootlesskit/pkg/port"
|
|
)
|
|
|
|
type Client interface {
|
|
HTTPClient() *http.Client
|
|
PortManager() port.Manager
|
|
}
|
|
|
|
// New creates a client.
|
|
// socketPath is a path to the UNIX socket, without unix:// prefix.
|
|
func New(socketPath string) (Client, error) {
|
|
if _, err := os.Stat(socketPath); err != nil {
|
|
return nil, err
|
|
}
|
|
hc := &http.Client{
|
|
Transport: &http.Transport{
|
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
var d net.Dialer
|
|
return d.DialContext(ctx, "unix", socketPath)
|
|
},
|
|
},
|
|
}
|
|
return NewWithHTTPClient(hc), nil
|
|
}
|
|
|
|
func NewWithHTTPClient(hc *http.Client) Client {
|
|
return &client{
|
|
Client: hc,
|
|
version: "v1",
|
|
dummyHost: "rootlesskit",
|
|
}
|
|
}
|
|
|
|
type client struct {
|
|
*http.Client
|
|
// version is always "v1"
|
|
// TODO(AkihiroSuda): negotiate the version
|
|
version string
|
|
dummyHost string
|
|
}
|
|
|
|
func (c *client) HTTPClient() *http.Client {
|
|
return c.Client
|
|
}
|
|
|
|
func (c *client) PortManager() port.Manager {
|
|
return &portManager{
|
|
client: c,
|
|
}
|
|
}
|
|
|
|
func readAtMost(r io.Reader, maxBytes int) ([]byte, error) {
|
|
lr := &io.LimitedReader{
|
|
R: r,
|
|
N: int64(maxBytes),
|
|
}
|
|
b, err := ioutil.ReadAll(lr)
|
|
if err != nil {
|
|
return b, err
|
|
}
|
|
if lr.N == 0 {
|
|
return b, errors.Errorf("expected at most %d bytes, got more", maxBytes)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// HTTPStatusErrorBodyMaxLength specifies the maximum length of HTTPStatusError.Body
|
|
const HTTPStatusErrorBodyMaxLength = 64 * 1024
|
|
|
|
// HTTPStatusError is created from non-2XX HTTP response
|
|
type HTTPStatusError struct {
|
|
// StatusCode is non-2XX status code
|
|
StatusCode int
|
|
// Body is at most HTTPStatusErrorBodyMaxLength
|
|
Body string
|
|
}
|
|
|
|
// Error implements error.
|
|
// If e.Body is a marshalled string of api.ErrorJSON, Error returns ErrorJSON.Message .
|
|
// Otherwise Error returns a human-readable string that contains e.StatusCode and e.Body.
|
|
func (e *HTTPStatusError) Error() string {
|
|
if e.Body != "" && len(e.Body) < HTTPStatusErrorBodyMaxLength {
|
|
var ej api.ErrorJSON
|
|
if json.Unmarshal([]byte(e.Body), &ej) == nil {
|
|
return ej.Message
|
|
}
|
|
}
|
|
return fmt.Sprintf("unexpected HTTP status %s, body=%q", http.StatusText(e.StatusCode), e.Body)
|
|
}
|
|
|
|
func successful(resp *http.Response) error {
|
|
if resp == nil {
|
|
return errors.New("nil response")
|
|
}
|
|
if resp.StatusCode/100 != 2 {
|
|
b, _ := readAtMost(resp.Body, HTTPStatusErrorBodyMaxLength)
|
|
return &HTTPStatusError{
|
|
StatusCode: resp.StatusCode,
|
|
Body: string(b),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type portManager struct {
|
|
*client
|
|
}
|
|
|
|
func (pm *portManager) AddPort(ctx context.Context, spec port.Spec) (*port.Status, error) {
|
|
m, err := json.Marshal(spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u := fmt.Sprintf("http://%s/%s/ports", pm.client.dummyHost, pm.client.version)
|
|
req, err := http.NewRequest("POST", u, bytes.NewReader(m))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(ctx)
|
|
resp, err := pm.client.HTTPClient().Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if err := successful(resp); err != nil {
|
|
return nil, err
|
|
}
|
|
dec := json.NewDecoder(resp.Body)
|
|
var status port.Status
|
|
if err := dec.Decode(&status); err != nil {
|
|
return nil, err
|
|
}
|
|
return &status, nil
|
|
}
|
|
func (pm *portManager) ListPorts(ctx context.Context) ([]port.Status, error) {
|
|
u := fmt.Sprintf("http://%s/%s/ports", pm.client.dummyHost, pm.client.version)
|
|
req, err := http.NewRequest("GET", u, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req = req.WithContext(ctx)
|
|
resp, err := pm.client.HTTPClient().Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if err := successful(resp); err != nil {
|
|
return nil, err
|
|
}
|
|
var statuses []port.Status
|
|
dec := json.NewDecoder(resp.Body)
|
|
if err := dec.Decode(&statuses); err != nil {
|
|
return nil, err
|
|
}
|
|
return statuses, nil
|
|
}
|
|
func (pm *portManager) RemovePort(ctx context.Context, id int) error {
|
|
u := fmt.Sprintf("http://%s/%s/ports/%d", pm.client.dummyHost, pm.client.version, id)
|
|
req, err := http.NewRequest("DELETE", u, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req = req.WithContext(ctx)
|
|
resp, err := pm.client.HTTPClient().Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if err := successful(resp); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|