166 lines
6.0 KiB
Go
166 lines
6.0 KiB
Go
package customer
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// WorkerRuntime is the minimal worker-host API used by Customer Service. A
|
|
// local Docker Engine and a remote Service Controller both implement the same
|
|
// contract, so public/customer/game APIs do not need to know where a worker is
|
|
// actually running.
|
|
type WorkerRuntime interface {
|
|
Ping(context.Context) error
|
|
EnsureImage(context.Context, string, bool, string) error
|
|
PullImage(context.Context, string, string) error
|
|
CreateVolume(context.Context, string) error
|
|
CreateWorker(context.Context, WorkerContainerConfig) (string, error)
|
|
Start(context.Context, string) error
|
|
Stop(context.Context, string, int) error
|
|
Remove(context.Context, string) error
|
|
Running(context.Context, string) (bool, error)
|
|
GetFile(context.Context, string, string) ([]byte, error)
|
|
PutFile(context.Context, string, string, string, []byte) error
|
|
RemoveVolume(context.Context, string) error
|
|
}
|
|
|
|
// RemoteControllerClient speaks the private Service-Controller API. It exposes
|
|
// the same logical methods as DockerClient while never granting Customer
|
|
// Service access to the remote host's docker.sock.
|
|
type RemoteControllerClient struct {
|
|
base string
|
|
secret string
|
|
hc *http.Client
|
|
}
|
|
|
|
func NewRemoteControllerClient(baseURL, secret string) (*RemoteControllerClient, error) {
|
|
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
|
if baseURL == "" {
|
|
return nil, errors.New("service controller base URL is empty")
|
|
}
|
|
u, err := url.Parse(baseURL)
|
|
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
|
return nil, fmt.Errorf("invalid service controller URL %q", baseURL)
|
|
}
|
|
if len(strings.TrimSpace(secret)) < 24 {
|
|
return nil, errors.New("service controller shared secret missing/too short")
|
|
}
|
|
return &RemoteControllerClient{base: baseURL, secret: strings.TrimSpace(secret), hc: &http.Client{Timeout: 45 * time.Second}}, nil
|
|
}
|
|
|
|
func (c *RemoteControllerClient) req(ctx context.Context, method, path string, in, out any) error {
|
|
var body io.Reader
|
|
if in != nil {
|
|
b, err := json.Marshal(in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body = bytes.NewReader(b)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, c.base+path, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+c.secret)
|
|
if in != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
client := c.hc
|
|
if strings.Contains(path, "/image/pull") {
|
|
clone := *c.hc
|
|
clone.Timeout = 11 * time.Minute
|
|
client = &clone
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("service controller %s: %w", c.base, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
b, readErr := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
|
|
if readErr != nil {
|
|
return readErr
|
|
}
|
|
if resp.StatusCode/100 != 2 {
|
|
var e struct {
|
|
Error string `json:"error"`
|
|
}
|
|
_ = json.Unmarshal(b, &e)
|
|
if strings.TrimSpace(e.Error) != "" {
|
|
return fmt.Errorf("service controller HTTP %d: %s", resp.StatusCode, strings.TrimSpace(e.Error))
|
|
}
|
|
return fmt.Errorf("service controller HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
|
|
}
|
|
if out != nil && len(bytes.TrimSpace(b)) > 0 {
|
|
if err := json.Unmarshal(b, out); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *RemoteControllerClient) Ping(ctx context.Context) error {
|
|
return c.req(ctx, http.MethodGet, "/internal/health", nil, nil)
|
|
}
|
|
func (c *RemoteControllerClient) EnsureImage(ctx context.Context, image string, autoPull bool, auth string) error {
|
|
return c.req(ctx, http.MethodPost, "/internal/image/ensure", map[string]any{"image": image, "auto_pull": autoPull, "registry_auth": auth}, nil)
|
|
}
|
|
func (c *RemoteControllerClient) PullImage(ctx context.Context, image, auth string) error {
|
|
return c.req(ctx, http.MethodPost, "/internal/image/pull", map[string]any{"image": image, "registry_auth": auth}, nil)
|
|
}
|
|
func (c *RemoteControllerClient) CreateVolume(ctx context.Context, name string) error {
|
|
return c.req(ctx, http.MethodPost, "/internal/volumes", map[string]string{"name": name}, nil)
|
|
}
|
|
func (c *RemoteControllerClient) CreateWorker(ctx context.Context, cfg WorkerContainerConfig) (string, error) {
|
|
var out struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := c.req(ctx, http.MethodPost, "/internal/workers", cfg, &out); err != nil {
|
|
return "", err
|
|
}
|
|
if strings.TrimSpace(out.ID) == "" {
|
|
return "", errors.New("service controller returned empty container id")
|
|
}
|
|
return out.ID, nil
|
|
}
|
|
func (c *RemoteControllerClient) Start(ctx context.Context, id string) error {
|
|
return c.req(ctx, http.MethodPost, "/internal/workers/"+url.PathEscape(id)+"/start", nil, nil)
|
|
}
|
|
func (c *RemoteControllerClient) Stop(ctx context.Context, id string, seconds int) error {
|
|
return c.req(ctx, http.MethodPost, "/internal/workers/"+url.PathEscape(id)+"/stop", map[string]int{"seconds": seconds}, nil)
|
|
}
|
|
func (c *RemoteControllerClient) Remove(ctx context.Context, id string) error {
|
|
return c.req(ctx, http.MethodDelete, "/internal/workers/"+url.PathEscape(id), nil, nil)
|
|
}
|
|
func (c *RemoteControllerClient) Running(ctx context.Context, id string) (bool, error) {
|
|
var out struct {
|
|
Running bool `json:"running"`
|
|
}
|
|
if err := c.req(ctx, http.MethodGet, "/internal/workers/"+url.PathEscape(id)+"/running", nil, &out); err != nil {
|
|
return false, err
|
|
}
|
|
return out.Running, nil
|
|
}
|
|
func (c *RemoteControllerClient) GetFile(ctx context.Context, id, path string) ([]byte, error) {
|
|
var out struct {
|
|
Data []byte `json:"data"`
|
|
}
|
|
if err := c.req(ctx, http.MethodPost, "/internal/workers/"+url.PathEscape(id)+"/file/get", map[string]string{"path": path}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out.Data, nil
|
|
}
|
|
func (c *RemoteControllerClient) PutFile(ctx context.Context, id, dir, name string, data []byte) error {
|
|
return c.req(ctx, http.MethodPost, "/internal/workers/"+url.PathEscape(id)+"/file/put", map[string]any{"dir": dir, "name": name, "data": data}, nil)
|
|
}
|
|
func (c *RemoteControllerClient) RemoveVolume(ctx context.Context, name string) error {
|
|
return c.req(ctx, http.MethodDelete, "/internal/volumes/"+url.PathEscape(name), nil, nil)
|
|
}
|