| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "fmt" |
| 7 | "io" |
| 8 | "net" |
| 9 | "net/http" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/activator" |
| 13 | "github.com/carlosframework/platform/internal/hostbox" |
| 14 | "github.com/carlosframework/platform/internal/registry" |
| 15 | ) |
| 16 | |
| 17 | // ClaimRoleRootClient is the agent's client of POST /claim/launch on the |
| 18 | // root instance-control socket. It implements activator.RootRoleLauncher. |
| 19 | type ClaimRoleRootClient struct { |
| 20 | Socket string |
| 21 | // Dial is the dialer; nil dials Socket as a unix socket. |
| 22 | Dial func(context.Context, string) (net.Conn, error) |
| 23 | } |
| 24 | |
| 25 | var _ activator.RootRoleLauncher = (*ClaimRoleRootClient)(nil) |
| 26 | |
| 27 | func NewClaimRoleRootClient(socket string) *ClaimRoleRootClient { |
| 28 | return &ClaimRoleRootClient{Socket: socket} |
| 29 | } |
| 30 | |
| 31 | // claimLaunchDeadline bounds one call: a start includes the unit start and |
| 32 | // root's checks around it. |
| 33 | const claimLaunchDeadline = 90 * time.Second |
| 34 | |
| 35 | func (c *ClaimRoleRootClient) call(ctx context.Context, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchResponse, error) { |
| 36 | if c == nil || c.Socket == "" { |
| 37 | return hostbox.RoleLaunchResponse{}, activator.ErrClaimIsolationUnavailable |
| 38 | } |
| 39 | body, err := hostbox.EncodeRoleLaunchRequest(q) |
| 40 | if err != nil { |
| 41 | return hostbox.RoleLaunchResponse{}, err |
| 42 | } |
| 43 | ctx, cancel := context.WithTimeout(ctx, claimLaunchDeadline) |
| 44 | defer cancel() |
| 45 | dial := c.Dial |
| 46 | if dial == nil { |
| 47 | dial = func(ctx context.Context, path string) (net.Conn, error) { |
| 48 | return (&net.Dialer{}).DialContext(ctx, "unix", path) |
| 49 | } |
| 50 | } |
| 51 | transport := &http.Transport{DisableKeepAlives: true, DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return dial(ctx, c.Socket) }} |
| 52 | defer transport.CloseIdleConnections() |
| 53 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://control/claim/launch", bytes.NewReader(body)) |
| 54 | if err != nil { |
| 55 | return hostbox.RoleLaunchResponse{}, err |
| 56 | } |
| 57 | req.Header.Set("Content-Type", "application/json") |
| 58 | resp, err := (&http.Client{Transport: transport}).Do(req) |
| 59 | if err != nil { |
| 60 | return hostbox.RoleLaunchResponse{}, fmt.Errorf("claim launch: %w", err) |
| 61 | } |
| 62 | defer resp.Body.Close() |
| 63 | switch resp.StatusCode { |
| 64 | case http.StatusOK: |
| 65 | return hostbox.DecodeRoleLaunchResponse(resp.Body) |
| 66 | case http.StatusBadRequest: |
| 67 | return hostbox.RoleLaunchResponse{}, hostbox.ErrRoleLaunchShape |
| 68 | case http.StatusForbidden: |
| 69 | return hostbox.RoleLaunchResponse{}, ErrClaimStopIdentity |
| 70 | case http.StatusConflict: |
| 71 | return hostbox.RoleLaunchResponse{}, hostbox.ErrChanged |
| 72 | case http.StatusServiceUnavailable, http.StatusNotFound, http.StatusMethodNotAllowed: |
| 73 | // An older root has no such route; a root without its providers |
| 74 | // refuses the same way. |
| 75 | return hostbox.RoleLaunchResponse{}, activator.ErrClaimIsolationUnavailable |
| 76 | } |
| 77 | _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4<<10)) |
| 78 | return hostbox.RoleLaunchResponse{}, fmt.Errorf("claim launch: root answered %d", resp.StatusCode) |
| 79 | } |
| 80 | |
| 81 | // Capability is the protocol root speaks; it grants nothing. |
| 82 | func (c *ClaimRoleRootClient) Capability(ctx context.Context) (string, error) { |
| 83 | out, err := c.call(ctx, hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchCapability}) |
| 84 | if err != nil { |
| 85 | return "", err |
| 86 | } |
| 87 | return out.Protocol, nil |
| 88 | } |
| 89 | |
| 90 | // Launch asks root to run q's reservation and returns root's grant for |
| 91 | // exactly that reservation. |
| 92 | func (c *ClaimRoleRootClient) Launch(ctx context.Context, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchGrant, error) { |
| 93 | if q.Op != hostbox.RoleLaunchStart || q.Reservation == nil { |
| 94 | return hostbox.RoleLaunchGrant{}, hostbox.ErrRoleLaunchShape |
| 95 | } |
| 96 | out, err := c.call(ctx, q) |
| 97 | if err != nil { |
| 98 | return hostbox.RoleLaunchGrant{}, err |
| 99 | } |
| 100 | if out.Protocol != hostbox.RoleLaunchProtocol || out.Grant == nil { |
| 101 | return hostbox.RoleLaunchGrant{}, hostbox.ErrChanged |
| 102 | } |
| 103 | if err := out.Grant.Check(*q.Reservation); err != nil { |
| 104 | return hostbox.RoleLaunchGrant{}, err |
| 105 | } |
| 106 | return *out.Grant, nil |
| 107 | } |
| 108 | |
| 109 | // Stop asks root to end q's reservation, or to spend it if root never saw it. |
| 110 | func (c *ClaimRoleRootClient) Stop(ctx context.Context, q hostbox.RoleLaunchRequest) error { |
| 111 | if q.Op != hostbox.RoleLaunchStop || q.Reservation == nil { |
| 112 | return hostbox.ErrRoleLaunchShape |
| 113 | } |
| 114 | _, err := c.call(ctx, q) |
| 115 | return err |
| 116 | } |
| 117 | |
| 118 | func (c *ClaimRoleRootClient) KindCapability(ctx context.Context, kind string) error { |
| 119 | out, err := c.call(ctx, hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchCapability, Kind: kind}) |
| 120 | if err != nil { |
| 121 | return err |
| 122 | } |
| 123 | if out.Protocol != hostbox.RoleLaunchProtocol { |
| 124 | return hostbox.ErrChanged |
| 125 | } |
| 126 | return nil |
| 127 | } |
| 128 | |
| 129 | func (c *ClaimRoleRootClient) UnitInputs(ctx context.Context, route registry.Route) (*hostbox.UnitLaunchInputs, error) { |
| 130 | out, err := c.call(ctx, hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchInputs, Route: &route}) |
| 131 | if err != nil { |
| 132 | return nil, err |
| 133 | } |
| 134 | if out.Protocol != hostbox.RoleLaunchProtocol { |
| 135 | return nil, hostbox.ErrChanged |
| 136 | } |
| 137 | return out.Inputs, nil |
| 138 | } |
| 139 | |