Files
coder/cli/sync_start.go
T
Atif Ali 882689a888 fix(cli): show sync wait dependencies (#25369)
## Summary
Backports #25089 to `release/2.32` so `coder exp sync want` and `coder
exp sync start` print the dependency units involved in startup
coordination instead of generic success messages.

## Validation
- `git diff --check origin/release/2.32..HEAD`
- `go test ./cli -run TestSyncCommands -count=1`

> [!NOTE]
> `make test RUN=TestSyncCommands` hit an unrelated `codersdk/toolsdk`
filtered-test failure because that package expects all tools to be
tested. The affected CLI test passed with the package-scoped command
above.

> 🤖 This PR was created with the help of Coder Agents, and needs a human
review. 🧑💻

Co-authored-by: Max Schwenk <maschwenk@gmail.com>
2026-05-18 08:55:10 -04:00

118 lines
2.9 KiB
Go

package cli
import (
"context"
"fmt"
"slices"
"strings"
"time"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/agent/agentsocket"
"github.com/coder/coder/v2/agent/unit"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/serpent"
)
const (
syncPollInterval = 1 * time.Second
)
func (*RootCmd) syncStart(socketPath *string) *serpent.Command {
var timeout time.Duration
cmd := &serpent.Command{
Use: "start <unit>",
Short: "Wait until all unit dependencies are satisfied",
Long: "Wait until all dependencies are satisfied, consider the unit to have started, then allow it to proceed. This command polls until dependencies are ready, then marks the unit as started.",
Handler: func(i *serpent.Invocation) error {
ctx := i.Context()
if len(i.Args) != 1 {
return xerrors.New("exactly one unit name is required")
}
unitName := unit.ID(i.Args[0])
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
opts := []agentsocket.Option{}
if *socketPath != "" {
opts = append(opts, agentsocket.WithPath(*socketPath))
}
client, err := agentsocket.NewClient(ctx, opts...)
if err != nil {
return xerrors.Errorf("connect to agent socket: %w", err)
}
defer client.Close()
statusResp, err := client.SyncStatus(ctx, unitName)
if err != nil {
return xerrors.Errorf("get status failed: %w", err)
}
ready := statusResp.IsReady
var waitedFor []string
if !ready {
for _, dep := range statusResp.Dependencies {
if !dep.IsSatisfied {
waitedFor = append(waitedFor, string(dep.DependsOn))
}
}
slices.Sort(waitedFor)
waitedForList := strings.Join(waitedFor, ", ")
cliui.Infof(i.Stdout, "Unit %q is waiting for dependencies to be satisfied: [%s]", unitName, waitedForList)
ticker := time.NewTicker(syncPollInterval)
defer ticker.Stop()
pollLoop:
for {
select {
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
return xerrors.Errorf("timeout waiting for dependencies of unit '%s'", unitName)
}
return ctx.Err()
case <-ticker.C:
ready, err := client.SyncReady(ctx, unitName)
if err != nil {
return xerrors.Errorf("error checking dependencies: %w", err)
}
if ready {
break pollLoop
}
}
}
}
if err := client.SyncStart(ctx, unitName); err != nil {
return xerrors.Errorf("start unit failed: %w", err)
}
if len(waitedFor) == 0 {
cliui.Info(i.Stdout, "Success")
} else {
cliui.Info(i.Stdout, fmt.Sprintf("Unit %q finished waiting for dependencies: [%s]", unitName, strings.Join(waitedFor, ", ")))
}
return nil
},
}
cmd.Options = append(cmd.Options, serpent.Option{
Flag: "timeout",
Description: "Maximum time to wait for dependencies (e.g., 30s, 5m). 5m by default.",
Value: serpent.DurationOf(&timeout),
Default: "5m",
})
return cmd
}