package sshpush import ( "context" "fmt" "os" "sync" "golang.org/x/sync/errgroup" "git.cloudinit.dev/coreci/orca/internal/emitter" ) // DefaultFanoutConcurrency is the default bounded-concurrency limit for // fan-out operations (I-B-001). The Transport.ExecAll and WriteAll // methods use this when the caller does not override it. const DefaultFanoutConcurrency = 8 // ExecAll runs cmd on all peers in parallel with bounded concurrency // (default 8, I-B-001). Returns per-peer output and per-peer errors. A // nil entry in the errors map means that peer succeeded; the output map // contains that peer's stdout. The returned error is non-nil only if // the fan-out itself failed (e.g., context cancelled before any peer // ran); per-peer failures are in the errors map. func (t *Transport) ExecAll(ctx context.Context, peers []string, cmd string) (map[string][]byte, map[string]error) { return t.ExecAllWithConcurrency(ctx, peers, cmd, DefaultFanoutConcurrency) } // ExecAllWithConcurrency is ExecAll with an explicit concurrency limit. // A limit <= 0 uses DefaultFanoutConcurrency. func (t *Transport) ExecAllWithConcurrency(ctx context.Context, peers []string, cmd string, concurrency int) (map[string][]byte, map[string]error) { if concurrency <= 0 { concurrency = DefaultFanoutConcurrency } out := make(map[string][]byte, len(peers)) errs := make(map[string]error, len(peers)) var mu sync.Mutex g, gctx := errgroup.WithContext(ctx) g.SetLimit(concurrency) for _, p := range peers { peer := p g.Go(func() error { o, err := t.Exec(gctx, peer, cmd) mu.Lock() defer mu.Unlock() if err != nil { errs[peer] = err return nil // per-peer error; do not cancel the group } out[peer] = o return nil }) } _ = g.Wait() return out, errs } // WriteAll writes the given files to each peer in parallel with bounded // concurrency (default 8, I-B-001). The files map is keyed by peer; each // peer's files are written sequentially (to preserve order and avoid // intra-peer races on shared paths). Returns per-peer errors; a peer // missing from the map or with a nil entry succeeded. The returned // error is non-nil only if the fan-out itself failed (context cancelled). func (t *Transport) WriteAll(ctx context.Context, peers []string, files map[string][]emitter.File) map[string]error { return t.WriteAllWithConcurrency(ctx, peers, files, DefaultFanoutConcurrency) } // WriteAllWithConcurrency is WriteAll with an explicit concurrency limit. // A limit <= 0 uses DefaultFanoutConcurrency. func (t *Transport) WriteAllWithConcurrency(ctx context.Context, peers []string, files map[string][]emitter.File, concurrency int) map[string]error { if concurrency <= 0 { concurrency = DefaultFanoutConcurrency } errs := make(map[string]error, len(peers)) var mu sync.Mutex g, gctx := errgroup.WithContext(ctx) g.SetLimit(concurrency) for _, p := range peers { peer := p peerFiles := files[peer] g.Go(func() error { for _, f := range peerFiles { mode := parseMode(f.Mode) if _, err := t.WriteFileIdempotent(gctx, peer, f.Path, []byte(f.Content), mode); err != nil { mu.Lock() errs[peer] = fmt.Errorf("sshpush: write %s on %s: %w", f.Path, peer, err) mu.Unlock() return nil // per-peer error; do not cancel the group } } return nil }) } _ = g.Wait() return errs } // parseMode parses an octal mode string like "0644" into an os.FileMode. // Returns 0644 on parse failure (a safe default for non-executable // config files). func parseMode(s string) os.FileMode { var m uint32 for _, r := range s { if r < '0' || r > '7' { return 0o644 } m = m<<3 | uint32(r-'0') } if m == 0 { return 0o644 } return os.FileMode(m) }