mirror of
https://github.com/coder/coder.git
synced 2026-06-04 21:48:22 +00:00
ce627bf23f
closes: https://github.com/coder/coder/issues/10352 closes: https://github.com/coder/internal/issues/1094 closes: https://github.com/coder/internal/issues/1095 In this pull request, we enable a new set of experimental cli commands grouped under `coder exp sync`. These commands allow any process acting within a coder workspace to inform the coder agent of its requirements and execution progress. The coder agent will then relay this information to other processes that have subscribed. These commands are: ``` # Check if this feature is enabled in your environment coder exp sync ping # express that your unit depends on another coder exp sync want <unit> <dependency_unit> # express that your unit intends to start a portion of the script that requires # other units to have completed first. This command blocks until all dependencies have been met coder exp sync start <unit> # express that your unit has completes its work, allowing dependent units to begin their execution coder exp sync complete <unit> ``` Example: In order to automatically run claude code in a new workspace, it must first have a git repository cloned. The scripts responsible for cloning the repository and for running claude code would coordinate in the following way: ```bash # Script A: Claude code # Inform the agent that the claude script wants the git script. # That is, the git script must have completed before the claude script can begin its execution coder exp sync want claude git # Inform the agent that we would now like to begin execution of claude. # This command will block until the git script (and any other defined dependencies) # have completed coder exp sync start claude # Now we run claude code and any other commands we need claude ... # Once our script has completed, we inform the agent, so that any scripts that depend on this one # may begin their execution coder exp sync complete claude ``` ```bash # Script B: Git # Because the git script does not have any dependencies, we can simply inform the agent that we # intend to start coder exp sync start git git clone ssh://git@github.com/coder/coder # Once the repository have been cloned, we inform the agent that this script is complete, so that # scripts that depend on it may begin their execution. coder exp sync complete git ``` Notes: * Unit names (ie. `claude` and `git`) given as input to the sync commands are arbitrary strings. You do not have to conform to specific identifiers. We recommend naming your scripts descriptively, but succinctly. * Scripts unit names should be well documented. Other scripts will need to know the names you've chosen in order to depend on yours. Therefore, you --------- Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
147 lines
4.1 KiB
Go
147 lines
4.1 KiB
Go
package agentsocket
|
|
|
|
import (
|
|
"context"
|
|
|
|
"golang.org/x/xerrors"
|
|
"storj.io/drpc"
|
|
"storj.io/drpc/drpcconn"
|
|
|
|
"github.com/coder/coder/v2/agent/agentsocket/proto"
|
|
"github.com/coder/coder/v2/agent/unit"
|
|
)
|
|
|
|
// Option represents a configuration option for NewClient.
|
|
type Option func(*options)
|
|
|
|
type options struct {
|
|
path string
|
|
}
|
|
|
|
// WithPath sets the socket path. If not provided or empty, the client will
|
|
// auto-discover the default socket path.
|
|
func WithPath(path string) Option {
|
|
return func(opts *options) {
|
|
if path == "" {
|
|
return
|
|
}
|
|
opts.path = path
|
|
}
|
|
}
|
|
|
|
// Client provides a client for communicating with the workspace agentsocket API.
|
|
type Client struct {
|
|
client proto.DRPCAgentSocketClient
|
|
conn drpc.Conn
|
|
}
|
|
|
|
// NewClient creates a new socket client and opens a connection to the socket.
|
|
// If path is not provided via WithPath or is empty, it will auto-discover the
|
|
// default socket path.
|
|
func NewClient(ctx context.Context, opts ...Option) (*Client, error) {
|
|
options := &options{}
|
|
for _, opt := range opts {
|
|
opt(options)
|
|
}
|
|
|
|
conn, err := dialSocket(ctx, options.path)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("connect to socket: %w", err)
|
|
}
|
|
|
|
drpcConn := drpcconn.New(conn)
|
|
client := proto.NewDRPCAgentSocketClient(drpcConn)
|
|
|
|
return &Client{
|
|
client: client,
|
|
conn: drpcConn,
|
|
}, nil
|
|
}
|
|
|
|
// Close closes the socket connection.
|
|
func (c *Client) Close() error {
|
|
return c.conn.Close()
|
|
}
|
|
|
|
// Ping sends a ping request to the agent.
|
|
func (c *Client) Ping(ctx context.Context) error {
|
|
_, err := c.client.Ping(ctx, &proto.PingRequest{})
|
|
return err
|
|
}
|
|
|
|
// SyncStart starts a unit in the dependency graph.
|
|
func (c *Client) SyncStart(ctx context.Context, unitName unit.ID) error {
|
|
_, err := c.client.SyncStart(ctx, &proto.SyncStartRequest{
|
|
Unit: string(unitName),
|
|
})
|
|
return err
|
|
}
|
|
|
|
// SyncWant declares a dependency between units.
|
|
func (c *Client) SyncWant(ctx context.Context, unitName, dependsOn unit.ID) error {
|
|
_, err := c.client.SyncWant(ctx, &proto.SyncWantRequest{
|
|
Unit: string(unitName),
|
|
DependsOn: string(dependsOn),
|
|
})
|
|
return err
|
|
}
|
|
|
|
// SyncComplete marks a unit as complete in the dependency graph.
|
|
func (c *Client) SyncComplete(ctx context.Context, unitName unit.ID) error {
|
|
_, err := c.client.SyncComplete(ctx, &proto.SyncCompleteRequest{
|
|
Unit: string(unitName),
|
|
})
|
|
return err
|
|
}
|
|
|
|
// SyncReady requests whether a unit is ready to be started. That is, all dependencies are satisfied.
|
|
func (c *Client) SyncReady(ctx context.Context, unitName unit.ID) (bool, error) {
|
|
resp, err := c.client.SyncReady(ctx, &proto.SyncReadyRequest{
|
|
Unit: string(unitName),
|
|
})
|
|
return resp.Ready, err
|
|
}
|
|
|
|
// SyncStatus gets the status of a unit and its dependencies.
|
|
func (c *Client) SyncStatus(ctx context.Context, unitName unit.ID) (SyncStatusResponse, error) {
|
|
resp, err := c.client.SyncStatus(ctx, &proto.SyncStatusRequest{
|
|
Unit: string(unitName),
|
|
})
|
|
if err != nil {
|
|
return SyncStatusResponse{}, err
|
|
}
|
|
|
|
var dependencies []DependencyInfo
|
|
for _, dep := range resp.Dependencies {
|
|
dependencies = append(dependencies, DependencyInfo{
|
|
DependsOn: unit.ID(dep.DependsOn),
|
|
RequiredStatus: unit.Status(dep.RequiredStatus),
|
|
CurrentStatus: unit.Status(dep.CurrentStatus),
|
|
IsSatisfied: dep.IsSatisfied,
|
|
})
|
|
}
|
|
|
|
return SyncStatusResponse{
|
|
UnitName: unitName,
|
|
Status: unit.Status(resp.Status),
|
|
IsReady: resp.IsReady,
|
|
Dependencies: dependencies,
|
|
}, nil
|
|
}
|
|
|
|
// SyncStatusResponse contains the status information for a unit.
|
|
type SyncStatusResponse struct {
|
|
UnitName unit.ID `table:"unit,default_sort" json:"unit_name"`
|
|
Status unit.Status `table:"status" json:"status"`
|
|
IsReady bool `table:"ready" json:"is_ready"`
|
|
Dependencies []DependencyInfo `table:"dependencies" json:"dependencies"`
|
|
}
|
|
|
|
// DependencyInfo contains information about a unit dependency.
|
|
type DependencyInfo struct {
|
|
DependsOn unit.ID `table:"depends on,default_sort" json:"depends_on"`
|
|
RequiredStatus unit.Status `table:"required status" json:"required_status"`
|
|
CurrentStatus unit.Status `table:"current status" json:"current_status"`
|
|
IsSatisfied bool `table:"satisfied" json:"is_satisfied"`
|
|
}
|