Files
coder/cli/sync_start.go
T
Max Schwenk f3e90b334d fix(cli): show sync wait dependencies (#25089)
## Problem
`coder exp sync want` and `coder exp sync start` both printed generic
success messages, which hid the dependency units involved in startup
coordination.

Before, declaring dependencies with `sync want` printed:

```text
Success
```

Before, `sync start` printed while waiting, then finished with another
generic success message:

```text
Waiting for dependencies of unit 'test-unit' to be satisfied...
Success
```

## Solution
Print the dependency units in both cases, using wording that matches
where the command is in the lifecycle.

After, `sync want` prints the dependencies it declared for the unit:

```text
Unit "test-unit" declared dependencies: [dep-unit]
```

After, `sync start` enumerates the dependencies while it is waiting,
then prints the same dependencies after the unit starts executing:

```text
Unit "test-unit" is waiting for dependencies to be satisfied: [dep-unit, dep-unit-2]
Unit "test-unit" finished waiting for dependencies: [dep-unit, dep-unit-2]
```

The sync golden tests now cover the updated output, including multiple
dependencies for `sync start`.
2026-05-14 14:45:20 +02: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
}