mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +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>
153 lines
4.7 KiB
Go
153 lines
4.7 KiB
Go
package agentsocket
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog"
|
|
"github.com/coder/coder/v2/agent/agentsocket/proto"
|
|
"github.com/coder/coder/v2/agent/unit"
|
|
)
|
|
|
|
var _ proto.DRPCAgentSocketServer = (*DRPCAgentSocketService)(nil)
|
|
|
|
var ErrUnitManagerNotAvailable = xerrors.New("unit manager not available")
|
|
|
|
// DRPCAgentSocketService implements the DRPC agent socket service.
|
|
type DRPCAgentSocketService struct {
|
|
unitManager *unit.Manager
|
|
logger slog.Logger
|
|
}
|
|
|
|
// Ping responds to a ping request to check if the service is alive.
|
|
func (*DRPCAgentSocketService) Ping(_ context.Context, _ *proto.PingRequest) (*proto.PingResponse, error) {
|
|
return &proto.PingResponse{}, nil
|
|
}
|
|
|
|
// SyncStart starts a unit in the dependency graph.
|
|
func (s *DRPCAgentSocketService) SyncStart(_ context.Context, req *proto.SyncStartRequest) (*proto.SyncStartResponse, error) {
|
|
if s.unitManager == nil {
|
|
return nil, xerrors.Errorf("SyncStart: %w", ErrUnitManagerNotAvailable)
|
|
}
|
|
|
|
unitID := unit.ID(req.Unit)
|
|
|
|
if err := s.unitManager.Register(unitID); err != nil {
|
|
if !errors.Is(err, unit.ErrUnitAlreadyRegistered) {
|
|
return nil, xerrors.Errorf("SyncStart: %w", err)
|
|
}
|
|
}
|
|
|
|
isReady, err := s.unitManager.IsReady(unitID)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot check readiness: %w", err)
|
|
}
|
|
if !isReady {
|
|
return nil, xerrors.Errorf("cannot start unit %q: unit not ready", req.Unit)
|
|
}
|
|
|
|
err = s.unitManager.UpdateStatus(unitID, unit.StatusStarted)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot start unit %q: %w", req.Unit, err)
|
|
}
|
|
|
|
return &proto.SyncStartResponse{}, nil
|
|
}
|
|
|
|
// SyncWant declares a dependency between units.
|
|
func (s *DRPCAgentSocketService) SyncWant(_ context.Context, req *proto.SyncWantRequest) (*proto.SyncWantResponse, error) {
|
|
if s.unitManager == nil {
|
|
return nil, xerrors.Errorf("cannot add dependency: %w", ErrUnitManagerNotAvailable)
|
|
}
|
|
|
|
unitID := unit.ID(req.Unit)
|
|
dependsOnID := unit.ID(req.DependsOn)
|
|
|
|
if err := s.unitManager.Register(unitID); err != nil && !errors.Is(err, unit.ErrUnitAlreadyRegistered) {
|
|
return nil, xerrors.Errorf("cannot add dependency: %w", err)
|
|
}
|
|
|
|
if err := s.unitManager.AddDependency(unitID, dependsOnID, unit.StatusComplete); err != nil {
|
|
return nil, xerrors.Errorf("cannot add dependency: %w", err)
|
|
}
|
|
|
|
return &proto.SyncWantResponse{}, nil
|
|
}
|
|
|
|
// SyncComplete marks a unit as complete in the dependency graph.
|
|
func (s *DRPCAgentSocketService) SyncComplete(_ context.Context, req *proto.SyncCompleteRequest) (*proto.SyncCompleteResponse, error) {
|
|
if s.unitManager == nil {
|
|
return nil, xerrors.Errorf("cannot complete unit: %w", ErrUnitManagerNotAvailable)
|
|
}
|
|
|
|
unitID := unit.ID(req.Unit)
|
|
|
|
if err := s.unitManager.UpdateStatus(unitID, unit.StatusComplete); err != nil {
|
|
return nil, xerrors.Errorf("cannot complete unit %q: %w", req.Unit, err)
|
|
}
|
|
|
|
return &proto.SyncCompleteResponse{}, nil
|
|
}
|
|
|
|
// SyncReady checks whether a unit is ready to be started. That is, all dependencies are satisfied.
|
|
func (s *DRPCAgentSocketService) SyncReady(_ context.Context, req *proto.SyncReadyRequest) (*proto.SyncReadyResponse, error) {
|
|
if s.unitManager == nil {
|
|
return nil, xerrors.Errorf("cannot check readiness: %w", ErrUnitManagerNotAvailable)
|
|
}
|
|
|
|
unitID := unit.ID(req.Unit)
|
|
isReady, err := s.unitManager.IsReady(unitID)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot check readiness: %w", err)
|
|
}
|
|
|
|
return &proto.SyncReadyResponse{
|
|
Ready: isReady,
|
|
}, nil
|
|
}
|
|
|
|
// SyncStatus gets the status of a unit and lists its dependencies.
|
|
func (s *DRPCAgentSocketService) SyncStatus(_ context.Context, req *proto.SyncStatusRequest) (*proto.SyncStatusResponse, error) {
|
|
if s.unitManager == nil {
|
|
return nil, xerrors.Errorf("cannot get status for unit %q: %w", req.Unit, ErrUnitManagerNotAvailable)
|
|
}
|
|
|
|
unitID := unit.ID(req.Unit)
|
|
|
|
isReady, err := s.unitManager.IsReady(unitID)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot check readiness: %w", err)
|
|
}
|
|
|
|
dependencies, err := s.unitManager.GetAllDependencies(unitID)
|
|
switch {
|
|
case errors.Is(err, unit.ErrUnitNotFound):
|
|
dependencies = []unit.Dependency{}
|
|
case err != nil:
|
|
return nil, xerrors.Errorf("cannot get dependencies: %w", err)
|
|
}
|
|
|
|
var depInfos []*proto.DependencyInfo
|
|
for _, dep := range dependencies {
|
|
depInfos = append(depInfos, &proto.DependencyInfo{
|
|
Unit: string(dep.Unit),
|
|
DependsOn: string(dep.DependsOn),
|
|
RequiredStatus: string(dep.RequiredStatus),
|
|
CurrentStatus: string(dep.CurrentStatus),
|
|
IsSatisfied: dep.IsSatisfied,
|
|
})
|
|
}
|
|
|
|
u, err := s.unitManager.Unit(unitID)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot get status for unit %q: %w", req.Unit, err)
|
|
}
|
|
return &proto.SyncStatusResponse{
|
|
Status: string(u.Status()),
|
|
IsReady: isReady,
|
|
Dependencies: depInfos,
|
|
}, nil
|
|
}
|