package host import ( "bytes" "context" "fmt" "io" "net" "net/http" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/registry" ) // ClaimRoleRootClient is the agent's client of POST /claim/launch on the // root instance-control socket. It implements activator.RootRoleLauncher. type ClaimRoleRootClient struct { Socket string // Dial is the dialer; nil dials Socket as a unix socket. Dial func(context.Context, string) (net.Conn, error) } var _ activator.RootRoleLauncher = (*ClaimRoleRootClient)(nil) func NewClaimRoleRootClient(socket string) *ClaimRoleRootClient { return &ClaimRoleRootClient{Socket: socket} } // claimLaunchDeadline bounds one call: a start includes the unit start and // root's checks around it. const claimLaunchDeadline = 90 * time.Second func (c *ClaimRoleRootClient) call(ctx context.Context, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchResponse, error) { if c == nil || c.Socket == "" { return hostbox.RoleLaunchResponse{}, activator.ErrClaimIsolationUnavailable } body, err := hostbox.EncodeRoleLaunchRequest(q) if err != nil { return hostbox.RoleLaunchResponse{}, err } ctx, cancel := context.WithTimeout(ctx, claimLaunchDeadline) defer cancel() dial := c.Dial if dial == nil { dial = func(ctx context.Context, path string) (net.Conn, error) { return (&net.Dialer{}).DialContext(ctx, "unix", path) } } transport := &http.Transport{DisableKeepAlives: true, DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return dial(ctx, c.Socket) }} defer transport.CloseIdleConnections() req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://control/claim/launch", bytes.NewReader(body)) if err != nil { return hostbox.RoleLaunchResponse{}, err } req.Header.Set("Content-Type", "application/json") resp, err := (&http.Client{Transport: transport}).Do(req) if err != nil { return hostbox.RoleLaunchResponse{}, fmt.Errorf("claim launch: %w", err) } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: return hostbox.DecodeRoleLaunchResponse(resp.Body) case http.StatusBadRequest: return hostbox.RoleLaunchResponse{}, hostbox.ErrRoleLaunchShape case http.StatusForbidden: return hostbox.RoleLaunchResponse{}, ErrClaimStopIdentity case http.StatusConflict: return hostbox.RoleLaunchResponse{}, hostbox.ErrChanged case http.StatusServiceUnavailable, http.StatusNotFound, http.StatusMethodNotAllowed: // An older root has no such route; a root without its providers // refuses the same way. return hostbox.RoleLaunchResponse{}, activator.ErrClaimIsolationUnavailable } _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4<<10)) return hostbox.RoleLaunchResponse{}, fmt.Errorf("claim launch: root answered %d", resp.StatusCode) } // Capability is the protocol root speaks; it grants nothing. func (c *ClaimRoleRootClient) Capability(ctx context.Context) (string, error) { out, err := c.call(ctx, hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchCapability}) if err != nil { return "", err } return out.Protocol, nil } // Launch asks root to run q's reservation and returns root's grant for // exactly that reservation. func (c *ClaimRoleRootClient) Launch(ctx context.Context, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchGrant, error) { if q.Op != hostbox.RoleLaunchStart || q.Reservation == nil { return hostbox.RoleLaunchGrant{}, hostbox.ErrRoleLaunchShape } out, err := c.call(ctx, q) if err != nil { return hostbox.RoleLaunchGrant{}, err } if out.Protocol != hostbox.RoleLaunchProtocol || out.Grant == nil { return hostbox.RoleLaunchGrant{}, hostbox.ErrChanged } if err := out.Grant.Check(*q.Reservation); err != nil { return hostbox.RoleLaunchGrant{}, err } return *out.Grant, nil } // Stop asks root to end q's reservation, or to spend it if root never saw it. func (c *ClaimRoleRootClient) Stop(ctx context.Context, q hostbox.RoleLaunchRequest) error { if q.Op != hostbox.RoleLaunchStop || q.Reservation == nil { return hostbox.ErrRoleLaunchShape } _, err := c.call(ctx, q) return err } func (c *ClaimRoleRootClient) KindCapability(ctx context.Context, kind string) error { out, err := c.call(ctx, hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchCapability, Kind: kind}) if err != nil { return err } if out.Protocol != hostbox.RoleLaunchProtocol { return hostbox.ErrChanged } return nil } func (c *ClaimRoleRootClient) UnitInputs(ctx context.Context, route registry.Route) (*hostbox.UnitLaunchInputs, error) { out, err := c.call(ctx, hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchInputs, Route: &route}) if err != nil { return nil, err } if out.Protocol != hostbox.RoleLaunchProtocol { return nil, hostbox.ErrChanged } return out.Inputs, nil }