• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sapslaj / mid / 31137948604

07 Aug 2026 01:25AM UTC coverage: 66.557% (+24.8%) from 41.734%
31137948604

push

github

sapslaj
fix: don't make it worse

2 of 2 new or added lines in 1 file covered. (100.0%)

19 existing lines in 5 files now uncovered.

8673 of 13031 relevant lines covered (66.56%)

577.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.8
/provider/executor/agent_executor.go
1
package executor
2

3
import (
4
        "context"
5
        "errors"
6
        "fmt"
7
        "io"
8
        "log/slog"
9
        "net"
10
        "os"
11
        "os/user"
12
        "strconv"
13
        "strings"
14
        "sync"
15
        "time"
16

17
        "github.com/pulumi/pulumi/sdk/v3/go/common/util/retry"
18
        "go.opentelemetry.io/otel/attribute"
19
        "go.opentelemetry.io/otel/codes"
20
        "go.opentelemetry.io/otel/trace"
21
        "golang.org/x/crypto/ssh"
22
        "golang.org/x/crypto/ssh/agent"
23

24
        midagent "github.com/sapslaj/mid/agent"
25
        "github.com/sapslaj/mid/agent/rpc"
26
        "github.com/sapslaj/mid/pkg/cast"
27
        "github.com/sapslaj/mid/pkg/hashstructure"
28
        p "github.com/sapslaj/mid/pkg/providerfw"
29
        "github.com/sapslaj/mid/pkg/ptr"
30
        "github.com/sapslaj/mid/pkg/syncmap"
31
        "github.com/sapslaj/mid/pkg/telemetry"
32
        "github.com/sapslaj/mid/provider/midtypes"
33
)
34

35
var (
36
        ErrUnreachable = errors.New("host is unreachable")
37

38
        ErrHostUnset = errors.New("host is not set in the connection configuration")
39
)
40

41
type ConnectionState struct {
42
        ID              uint64
43
        Reachable       bool
44
        Unreachable     bool
45
        MaxParallel     int
46
        TaskCount       int
47
        SetupAgentMutex sync.Mutex
48
        CanConnectMutex sync.Mutex
49
        TaskCountMutex  sync.Mutex
50
        Agent           *midagent.Agent
51
        Connection      midtypes.Connection
52
}
53

54
var AgentPool = syncmap.Map[uint64, *ConnectionState]{}
55

56
func (cs *ConnectionState) SetupAgent(ctx context.Context) error {
316✔
57
        ctx, span := Tracer.Start(ctx, "mid/provider/executor.ConnectionState.SetupAgent", trace.WithAttributes(
316✔
58
                attribute.String("exec.strategy", "rpc"),
316✔
59
                attribute.String("connection.host", *cs.Connection.Host),
316✔
60
        ))
316✔
61
        defer span.End()
316✔
62
        logger := telemetry.LoggerFromContext(ctx).With(
316✔
63
                slog.String("connection.host", *cs.Connection.Host),
316✔
64
        )
316✔
65

316✔
66
        logger.DebugContext(ctx, "SetupAgent: waiting for lock")
316✔
67
        p.GetLogger(ctx).InfoStatus("waiting for existing connection attempts to finish...")
316✔
68
        cs.SetupAgentMutex.Lock()
316✔
69
        logger.DebugContext(ctx, "SetupAgent: lock acquired")
316✔
70
        p.GetLogger(ctx).InfoStatus("") // clear info line
316✔
71
        defer cs.SetupAgentMutex.Unlock()
316✔
72

316✔
73
        if cs.Agent != nil && cs.Agent.Running.Load() {
558✔
74
                logger.With(slog.Bool("agent.already_running", true)).DebugContext(ctx, "SetupAgent: agent is already running")
242✔
75
                span.SetAttributes(attribute.Bool("agent.already_running", true))
242✔
76
                span.SetStatus(codes.Ok, "")
242✔
77
                return nil
242✔
78
        }
242✔
79

80
        span.SetAttributes(
75✔
81
                attribute.Bool("agent.already_running", false),
75✔
82
                attribute.Bool("agent.reachable", cs.Reachable),
75✔
83
                attribute.Bool("agent.unreachable", cs.Unreachable),
75✔
84
        )
75✔
85

75✔
86
        logger = logger.With(
75✔
87
                slog.Bool("agent.already_running", false),
75✔
88
                slog.Bool("agent.reachable", cs.Reachable),
75✔
89
                slog.Bool("agent.unreachable", cs.Unreachable),
75✔
90
        )
75✔
91

75✔
92
        if cs.Unreachable {
75✔
93
                logger.WarnContext(ctx, "SetupAgent: remote previously deemed unreachable")
×
94
                err := ErrUnreachable
×
95
                span.SetStatus(codes.Error, err.Error())
×
96
                return err
×
97
        }
×
98

99
        sshConfig, endpoint, err := ConnectionToSSHClientConfig(cs.Connection)
75✔
100
        if err != nil {
75✔
101
                logger.ErrorContext(ctx, "SetupAgent: error building SSH config", slog.Any("error", err))
×
102
                span.SetStatus(codes.Error, err.Error())
×
103
                return err
×
104
        }
×
105

106
        sshClient, err := DialWithRetry(ctx, "Dial", 10, func() (*ssh.Client, error) {
150✔
107
                return ssh.Dial("tcp", endpoint, sshConfig)
75✔
108
        })
75✔
109
        if err != nil {
75✔
110
                logger.ErrorContext(ctx, "SetupAgent: error dialing", slog.Any("error", err))
×
111
                cs.Reachable = false
×
112
                cs.Unreachable = true
×
113
                span.SetStatus(codes.Error, err.Error())
×
114
                return errors.Join(ErrUnreachable, err)
×
115
        }
×
116

117
        cs.Agent = &midagent.Agent{
75✔
118
                Client: sshClient,
75✔
119
        }
75✔
120

75✔
121
        err = midagent.Connect(ctx, cs.Agent)
75✔
122
        if err != nil {
75✔
UNCOV
123
                logger.ErrorContext(ctx, "SetupAgent: error setting up agent", slog.Any("error", err))
×
UNCOV
124
                span.SetStatus(codes.Error, err.Error())
×
UNCOV
125
                return err
×
UNCOV
126
        }
×
127

128
        cs.Reachable = true
75✔
129
        span.SetAttributes(
75✔
130
                attribute.Bool("agent.running", true),
75✔
131
                attribute.Bool("agent.can_connect", cs.Reachable),
75✔
132
        )
75✔
133

75✔
134
        if cs.MaxParallel == 0 {
150✔
135
                nprocOutput, err := midagent.RunRemoteCommand(ctx, cs.Agent, "nproc")
75✔
136
                if err != nil {
75✔
137
                        logger.ErrorContext(ctx, "SetupAgent: error calling nproc", slog.Any("error", err))
×
138
                        span.SetStatus(codes.Error, err.Error())
×
139
                        return err
×
140
                }
×
141
                cs.MaxParallel, err = strconv.Atoi(strings.TrimSpace(string(nprocOutput)))
75✔
142
                if err != nil {
75✔
143
                        logger.ErrorContext(ctx, "SetupAgent: error parsing nproc output", slog.Any("error", err))
×
144
                        span.SetStatus(codes.Error, err.Error())
×
145
                        return err
×
146
                }
×
147
        }
148

149
        logger.DebugContext(ctx, "SetupAgent: finished agent setup")
75✔
150
        span.SetStatus(codes.Ok, "")
75✔
151
        return nil
75✔
152
}
153

154
func (cs *ConnectionState) FinishedTask() {
528✔
155
        cs.TaskCountMutex.Lock()
528✔
156
        cs.TaskCount = max(cs.TaskCount-1, 0)
528✔
157
        cs.TaskCountMutex.Unlock()
528✔
158
}
528✔
159

160
func Acquire(
161
        ctx context.Context,
162
        connection midtypes.Connection,
163
        resourceConfig midtypes.ResourceConfig,
164
) (*ConnectionState, error) {
528✔
165
        ctx, span := Tracer.Start(ctx, "mid/provider/executor.Acquire", trace.WithAttributes(
528✔
166
                attribute.String("exec.strategy", "rpc"),
528✔
167
        ))
528✔
168
        defer span.End()
528✔
169
        logger := telemetry.LoggerFromContext(ctx).With()
528✔
170

528✔
171
        if connection.Host == nil {
529✔
172
                logger.ErrorContext(ctx, "Acquire: host not set")
1✔
173
                err := errors.Join(ErrUnreachable, ErrHostUnset)
1✔
174
                span.SetStatus(codes.Error, err.Error())
1✔
175
                return nil, err
1✔
176
        }
1✔
177

178
        span.SetAttributes(
528✔
179
                attribute.String("connection.host", *connection.Host),
528✔
180
        )
528✔
181
        logger = logger.With(
528✔
182
                slog.String("connection.host", *connection.Host),
528✔
183
        )
528✔
184

528✔
185
        logger.DebugContext(ctx, "Acquire: calculating connection ID")
528✔
186
        id, err := hashstructure.Hash(connection, hashstructure.FormatV2, nil)
528✔
187
        if err != nil {
528✔
188
                span.SetStatus(codes.Error, err.Error())
×
189
                return nil, err
×
190
        }
×
191
        span.SetAttributes(attribute.Float64("agent.connection_id", float64(id)))
528✔
192
        logger = logger.With(slog.Uint64("agent.connection_id", id))
528✔
193

528✔
194
        logger.DebugContext(ctx, "Acquire: querying pool")
528✔
195

528✔
196
        cs, loaded := AgentPool.LoadOrStore(id, &ConnectionState{
528✔
197
                ID:         id,
528✔
198
                Connection: connection,
528✔
199
        })
528✔
200

528✔
201
        if !loaded && cs.MaxParallel == 0 {
603✔
202
                cs.MaxParallel = resourceConfig.GetParallel()
75✔
203
        }
75✔
204

205
        logger = logger.With(slog.Bool("agent.loaded", loaded))
528✔
206
        span.SetAttributes(attribute.Bool("agent.loaded", loaded))
528✔
207

528✔
208
        if cs.Agent != nil && cs.MaxParallel > 0 {
948✔
209
                logger.DebugContext(ctx, "Acquire: MaxParallel is set, spinlocking")
420✔
210
                for {
840✔
211
                        cs.TaskCountMutex.Lock()
420✔
212
                        if cs.TaskCount <= cs.MaxParallel {
840✔
213
                                cs.TaskCountMutex.Unlock()
420✔
214
                                break
420✔
215
                        }
216
                        cs.TaskCountMutex.Unlock()
×
217
                        time.Sleep(time.Millisecond)
×
218
                }
219
                logger.DebugContext(ctx, "Acquire: spinlock finished")
420✔
220
        }
221

222
        logger.DebugContext(ctx, "Acquire: returning ConnectionState handle")
528✔
223

528✔
224
        span.SetStatus(codes.Ok, "")
528✔
225
        return cs, nil
528✔
226
}
227

228
func CanConnect(
229
        ctx context.Context,
230
        connection midtypes.Connection,
231
        resourceConfig midtypes.ResourceConfig,
232
        maxAttempts int,
233
) (bool, error) {
213✔
234
        ctx, span := Tracer.Start(ctx, "mid/provider/executor.CanConnect", trace.WithAttributes(
213✔
235
                attribute.String("exec.strategy", "rpc"),
213✔
236
                attribute.Int("retry.max_attempts", maxAttempts),
213✔
237
        ))
213✔
238
        defer span.End()
213✔
239
        logger := telemetry.LoggerFromContext(ctx).With()
213✔
240

213✔
241
        if connection.Host == nil {
213✔
242
                logger.ErrorContext(ctx, "CanConnect: host not set")
×
243
                err := errors.Join(ErrUnreachable, ErrHostUnset)
×
244
                span.SetStatus(codes.Error, err.Error())
×
245
                return false, err
×
246
        }
×
247

248
        span.SetAttributes(
213✔
249
                attribute.String("connection.host", *connection.Host),
213✔
250
        )
213✔
251
        logger = logger.With(
213✔
252
                slog.String("connection.host", *connection.Host),
213✔
253
        )
213✔
254

213✔
255
        logger.DebugContext(ctx, "CanConnect: acquiring ConnectionState handle")
213✔
256
        cs, err := Acquire(ctx, connection, resourceConfig)
213✔
257
        if err != nil {
213✔
258
                span.SetStatus(codes.Error, err.Error())
×
259
                return false, err
×
260
        }
×
261
        defer cs.FinishedTask()
213✔
262

213✔
263
        logger.DebugContext(ctx, "CanConnect: waiting for lock")
213✔
264
        p.GetLogger(ctx).InfoStatus("waiting for existing connection attempts to finish...")
213✔
265
        cs.CanConnectMutex.Lock()
213✔
266
        logger.DebugContext(ctx, "CanConnect: lock acquired")
213✔
267
        p.GetLogger(ctx).InfoStatus("") // clear info line
213✔
268
        defer cs.CanConnectMutex.Unlock()
213✔
269

213✔
270
        if cs.Unreachable {
213✔
271
                span.SetAttributes(
×
272
                        attribute.Bool("agent.can_connect", false),
×
273
                        attribute.Bool("agent.can_connect.cached", true),
×
274
                )
×
275
                logger.With(
×
276
                        slog.Bool("agent.can_connect", false),
×
277
                        slog.Bool("agent.can_connect.cached", true),
×
278
                ).ErrorContext(ctx, "CanConnect: remote previously deemed unreachable")
×
279
                return false, ErrUnreachable
×
280
        }
×
281

282
        if cs.Reachable {
392✔
283
                span.SetAttributes(
179✔
284
                        attribute.Bool("agent.can_connect", true),
179✔
285
                        attribute.Bool("agent.can_connect.cached", true),
179✔
286
                )
179✔
287
                logger.With(
179✔
288
                        slog.Bool("agent.can_connect", true),
179✔
289
                        slog.Bool("agent.can_connect.cached", true),
179✔
290
                ).DebugContext(ctx, "CanConnect: remote previously deemed reachable")
179✔
291
                return true, nil
179✔
292
        }
179✔
293

294
        span.SetAttributes(
35✔
295
                attribute.Bool("agent.can_connect", false),
35✔
296
                attribute.Bool("agent.can_connect.cached", false),
35✔
297
        )
35✔
298
        logger = logger.With(slog.Bool("agent.can_connect.cached", false))
35✔
299

35✔
300
        if cs.Connection.Host == nil {
35✔
301
                cs.Reachable = false
×
302
                cs.Unreachable = true
×
303
                logger.With(
×
304
                        slog.Bool("agent.can_connect", false),
×
305
                ).ErrorContext(ctx, "CanConnect: host is nil")
×
306
                return false, nil
×
307
        }
×
308
        if *cs.Connection.Host == "" {
35✔
309
                cs.Reachable = false
×
310
                cs.Unreachable = true
×
311
                logger.With(
×
312
                        slog.Bool("agent.can_connect", false),
×
313
                ).ErrorContext(ctx, "CanConnect: host is empty")
×
314
                return false, nil
×
315
        }
×
316

317
        logger.DebugContext(ctx, "CanConnect: attempting connection")
35✔
318
        p.GetLogger(ctx).InfoStatus("attempting connection...")
35✔
319

35✔
320
        sshConfig, endpoint, err := ConnectionToSSHClientConfig(cs.Connection)
35✔
321
        if err != nil {
35✔
322
                logger.With(
×
323
                        slog.Bool("agent.can_connect", false),
×
324
                ).ErrorContext(ctx, "CanConnect: error building SSH config", slog.Any("error", err))
×
325
                span.SetStatus(codes.Error, err.Error())
×
326
                cs.Reachable = false
×
327
                cs.Unreachable = true
×
328
                return false, err
×
329
        }
×
330
        sshClient, err := DialWithRetry(ctx, "Dial", maxAttempts, func() (*ssh.Client, error) {
70✔
331
                return ssh.Dial("tcp", endpoint, sshConfig)
35✔
332
        })
35✔
333
        if err != nil {
35✔
334
                logger.With(
×
335
                        slog.Bool("agent.can_connect", false),
×
336
                ).ErrorContext(ctx, "CanConnect: error dialing", slog.Any("error", err))
×
337
                span.SetStatus(codes.Error, err.Error())
×
338
                cs.Reachable = false
×
339
                cs.Unreachable = true
×
340
                return false, errors.Join(ErrUnreachable, err)
×
341
        }
×
342
        defer sshClient.Close()
35✔
343
        session, err := sshClient.NewSession()
35✔
344
        if err != nil {
35✔
345
                logger.With(
×
346
                        slog.Bool("agent.can_connect", false),
×
347
                ).ErrorContext(ctx, "CanConnect: error creating agent session", slog.Any("error", err))
×
348
                span.SetStatus(codes.Error, err.Error())
×
349
                cs.Reachable = false
×
350
                cs.Unreachable = true
×
351
                return false, errors.Join(ErrUnreachable, err)
×
352
        }
×
353
        defer session.Close()
35✔
354

35✔
355
        logger.With(
35✔
356
                slog.Bool("agent.can_connect", true),
35✔
357
        ).DebugContext(ctx, "CanConnect: agent is reachable", slog.Any("error", err))
35✔
358
        span.SetStatus(codes.Ok, "")
35✔
359
        cs.Reachable = true
35✔
360
        cs.Unreachable = false
35✔
361
        span.SetAttributes(attribute.Bool("agent.can_connect", cs.Reachable))
35✔
362
        return cs.Reachable, nil
35✔
363
}
364

365
func PreviewUnreachable(
366
        ctx context.Context,
367
        connection midtypes.Connection,
368
        resourceConfig midtypes.ResourceConfig,
369
        preview bool,
370
) bool {
203✔
371
        ctx, span := Tracer.Start(ctx, "mid/provider/executor.PreviewUnreachable", trace.WithAttributes(
203✔
372
                attribute.String("exec.strategy", "rpc"),
203✔
373
                attribute.Bool("preview", preview),
203✔
374
        ))
203✔
375
        defer span.End()
203✔
376
        logger := telemetry.LoggerFromContext(ctx).With(
203✔
377
                slog.Bool("preview", preview),
203✔
378
        )
203✔
379

203✔
380
        if connection.Host != nil {
406✔
381
                span.SetAttributes(
203✔
382
                        attribute.String("connection.host", *connection.Host),
203✔
383
                )
203✔
384
                logger = logger.With(
203✔
385
                        slog.String("connection.host", *connection.Host),
203✔
386
                )
203✔
387
        } else if preview {
205✔
388
                logger.WarnContext(ctx, "PreviewUnreachable: host not set")
1✔
389
                span.SetStatus(codes.Ok, "")
1✔
390
                return true
1✔
391
        }
1✔
392

393
        // if preview: attempt connection and return false if unreachable but true if reachable
394
        // if not preview: attempt connection but always return false
395

396
        connectAttempts := 10
203✔
397
        if preview {
290✔
398
                connectAttempts = 4
87✔
399
        }
87✔
400

401
        logger.DebugContext(
203✔
402
                ctx,
203✔
403
                fmt.Sprintf("PreviewUnreachable: using connection attempts: %d", connectAttempts),
203✔
404
                slog.Int("connection_attempts", connectAttempts),
203✔
405
        )
203✔
406

203✔
407
        canConnect, err := CanConnect(ctx, connection, resourceConfig, connectAttempts)
203✔
408

203✔
409
        span.SetAttributes(attribute.Bool("agent.can_connect", canConnect))
203✔
410

203✔
411
        if err != nil {
203✔
412
                span.SetAttributes(attribute.String("agent.can_connect.error", err.Error()))
×
413
        }
×
414

415
        span.SetStatus(codes.Ok, "")
203✔
416

203✔
417
        if canConnect {
406✔
418
                logger.DebugContext(ctx, "PreviewUnreachable: connection attempt succeeded")
203✔
419
        } else if preview {
203✔
420
                logger.WarnContext(ctx, "PreviewUnreachable: connection attempt failed")
×
421
        } else {
×
422
                logger.ErrorContext(ctx, "PreviewUnreachable: connection attempt failed")
×
423
        }
×
424

425
        if !preview {
320✔
426
                return false
117✔
427
        }
117✔
428

429
        return !canConnect
87✔
430
}
431

432
func CallAgent[I any, O any](
433
        ctx context.Context,
434
        connection midtypes.Connection,
435
        resourceConfig midtypes.ResourceConfig,
436
        call rpc.RPCCall[I],
437
) (rpc.RPCResult[O], error) {
304✔
438
        ctx, span := Tracer.Start(ctx, "mid/provider/executor.CallAgent", trace.WithAttributes(
304✔
439
                attribute.String("exec.strategy", "rpc"),
304✔
440
                attribute.String("rpc.function", string(call.RPCFunction)),
304✔
441
                telemetry.OtelJSON("rpc.args", call.Args),
304✔
442
        ))
304✔
443
        defer span.End()
304✔
444
        logger := telemetry.LoggerFromContext(ctx).With(
304✔
445
                slog.String("rpc.function", string(call.RPCFunction)),
304✔
446
        )
304✔
447

304✔
448
        logger.DebugContext(
304✔
449
                ctx,
304✔
450
                fmt.Sprintf("CallAgent: calling RPC function %q", string(call.RPCFunction)),
304✔
451
                telemetry.SlogJSON("call", call),
304✔
452
        )
304✔
453

304✔
454
        var zero rpc.RPCResult[O]
304✔
455

304✔
456
        cs, err := Acquire(ctx, connection, resourceConfig)
304✔
457
        if err != nil {
305✔
458
                span.SetStatus(codes.Error, err.Error())
1✔
459
                return zero, err
1✔
460
        }
1✔
461
        defer cs.FinishedTask()
304✔
462

304✔
463
        if cs.Unreachable {
304✔
464
                err = ErrUnreachable
×
465
                span.SetStatus(codes.Error, err.Error())
×
466
                return zero, err
×
467
        }
×
468

469
        err = cs.SetupAgent(ctx)
304✔
470
        if err != nil {
304✔
UNCOV
471
                span.SetStatus(codes.Error, err.Error())
×
UNCOV
472
                return zero, err
×
UNCOV
473
        }
×
474

475
        res, err := midagent.Call[I, O](ctx, cs.Agent, call)
304✔
476
        if err == nil {
608✔
477
                span.SetStatus(codes.Ok, "")
304✔
478
        } else {
304✔
479
                span.SetStatus(codes.Error, err.Error())
×
480
        }
×
481

482
        span.SetAttributes(
304✔
483
                attribute.String("rpc.uuid", res.UUID),
304✔
484
                telemetry.OtelJSON("rpc.result", res.Result),
304✔
485
        )
304✔
486

304✔
487
        if res.Error != "" || err != nil {
305✔
488
                logger.ErrorContext(
1✔
489
                        ctx,
1✔
490
                        "CallAgent: got result",
1✔
491
                        slog.Any("error", err),
1✔
492
                        slog.String("rpc.error", res.Error),
1✔
493
                        telemetry.SlogJSON("rpc.result", res),
1✔
494
                )
1✔
495
        } else {
304✔
496
                logger.DebugContext(
303✔
497
                        ctx,
303✔
498
                        "CallAgent: got result",
303✔
499
                        telemetry.SlogJSON("rpc.result", res),
303✔
500
                )
303✔
501
        }
303✔
502

503
        if res.Error != "" {
305✔
504
                span.SetAttributes(attribute.String("rpc.error", res.Error))
1✔
505
        }
1✔
506

507
        return res, err
304✔
508
}
509

510
type AnsibleExecuteArgs interface {
511
        ToRPCCall() (rpc.RPCCall[rpc.AnsibleExecuteArgs], error)
512
}
513

514
type AnsibleExecuteReturn interface {
515
        IsChanged() bool
516
        GetMsg() string
517
}
518

519
func AnsibleExecute[I AnsibleExecuteArgs, O AnsibleExecuteReturn](
520
        ctx context.Context,
521
        connection midtypes.Connection,
522
        resourceConfig midtypes.ResourceConfig,
523
        args I,
524
        preview bool,
525
) (O, error) {
128✔
526
        ctx, span := Tracer.Start(ctx, "mid/provider/executor.AnsibleExecute", trace.WithAttributes(
128✔
527
                attribute.String("exec.strategy", "rpc"),
128✔
528
                telemetry.OtelJSON("args", args),
128✔
529
                attribute.Bool("preview", preview),
128✔
530
        ))
128✔
531
        defer span.End()
128✔
532
        logger := telemetry.LoggerFromContext(ctx).With(
128✔
533
                slog.Bool("preview", preview),
128✔
534
        )
128✔
535

128✔
536
        if connection.Host != nil {
256✔
537
                span.SetAttributes(
128✔
538
                        attribute.String("connection.host", *connection.Host),
128✔
539
                )
128✔
540
                logger = logger.With(
128✔
541
                        slog.String("connection.host", *connection.Host),
128✔
542
                )
128✔
543
        }
128✔
544

545
        logger.DebugContext(
128✔
546
                ctx,
128✔
547
                "AnsibleExecute: executing task",
128✔
548
                telemetry.SlogJSON("args", args),
128✔
549
        )
128✔
550

128✔
551
        var zero O
128✔
552

128✔
553
        call, err := args.ToRPCCall()
128✔
554
        if err != nil {
128✔
555
                span.SetStatus(codes.Error, err.Error())
×
556
                logger.ErrorContext(ctx, "AnsibleExecute: failed to convert args to RPC call", slog.Any("error", err))
×
557
                return zero, err
×
558
        }
×
559
        call.Args.Check = preview
128✔
560

128✔
561
        span.SetAttributes(attribute.String("ansible.name", call.Args.Name))
128✔
562

128✔
563
        if PreviewUnreachable(ctx, connection, resourceConfig, preview) {
128✔
564
                err = ErrUnreachable
×
565
                span.SetAttributes(attribute.Bool("unreachable", true))
×
566
                span.SetAttributes(attribute.Bool("ansible.success", false))
×
567
                span.SetStatus(codes.Error, err.Error())
×
568
                logger.WarnContext(ctx, "AnsibleExecute: bailing early due to unreachable host")
×
569
                return zero, err
×
570
        }
×
571

572
        callResult, err := CallAgent[
128✔
573
                rpc.AnsibleExecuteArgs,
128✔
574
                rpc.AnsibleExecuteResult,
128✔
575
        ](
128✔
576
                ctx,
128✔
577
                connection,
128✔
578
                resourceConfig,
128✔
579
                call,
128✔
580
        )
128✔
581
        if err != nil {
128✔
582
                span.SetAttributes(attribute.Bool("ansible.success", false))
×
583
                span.SetStatus(codes.Error, err.Error())
×
584
                logger.ErrorContext(ctx, "AnsibleExecute: failed to call agent", slog.Any("error", err))
×
585
                return zero, err
×
586
        }
×
587

588
        span.SetAttributes(
128✔
589
                attribute.Bool("ansible.success", callResult.Result.Success),
128✔
590
                telemetry.OtelJSON("ansible.call_result", callResult),
128✔
591
        )
128✔
592

128✔
593
        if !callResult.Result.Success {
128✔
594
                logger.WarnContext(ctx, "AnsibleExecute: not successful, extracting error information")
×
595
                maybeReturn, maybeReturnErr := cast.AnyToJSONT[O](callResult.Result.Result)
×
596
                if maybeReturnErr != nil {
×
597
                        span.SetAttributes(
×
598
                                attribute.String("ansible.return.decode_error", maybeReturnErr.Error()),
×
599
                        )
×
600
                        logger.WarnContext(ctx, "AnsibleExecute: call result conversion failed", slog.Any("error", err))
×
601
                }
×
602

603
                msg := maybeReturn.GetMsg()
×
604
                if msg != "" {
×
605
                        logger.DebugContext(ctx, "AnsibleExecute: using msg for error string", slog.String("msg", msg))
×
606
                        err = fmt.Errorf("error running module %q: %s", call.Args.Name, msg)
×
607
                } else {
×
608
                        err = fmt.Errorf(
×
609
                                "error running module %q: stderr=%s stdout=%s",
×
610
                                call.Args.Name,
×
611
                                callResult.Result.Stderr,
×
612
                                callResult.Result.Stdout,
×
613
                        )
×
614
                        logger.WarnContext(
×
615
                                ctx,
×
616
                                "AnsibleExecute: no msg found, using stderr and stdout",
×
617
                                slog.String("stderr", string(callResult.Result.Stderr)),
×
618
                                slog.String("stdout", string(callResult.Result.Stdout)),
×
619
                        )
×
620
                }
×
621

622
                span.SetAttributes(
×
623
                        attribute.String("ansible.msg", msg),
×
624
                        telemetry.OtelJSON("ansible.return", maybeReturn),
×
625
                )
×
626
                span.SetStatus(codes.Error, err.Error())
×
627
                logger.DebugContext(
×
628
                        ctx,
×
629
                        "AnsibleExecute: returning errored result",
×
630
                        slog.Any("error", err),
×
631
                        telemetry.SlogJSON("return", maybeReturn),
×
632
                )
×
633
                return maybeReturn, err
×
634
        }
635

636
        returns, err := cast.AnyToJSONT[O](callResult.Result.Result)
128✔
637
        span.SetAttributes(
128✔
638
                telemetry.OtelJSON("ansible.return", returns),
128✔
639
                attribute.String("ansible.msg", returns.GetMsg()),
128✔
640
                attribute.Bool("ansible.is_changed", returns.IsChanged()),
128✔
641
        )
128✔
642
        if err != nil {
128✔
643
                logger.ErrorContext(
×
644
                        ctx,
×
645
                        "AnsibleExecute: error decoding result",
×
646
                        slog.Any("error", err),
×
647
                        telemetry.SlogJSON("return", returns),
×
648
                )
×
649
                span.SetAttributes(
×
650
                        attribute.String("ansible.return.decode_error", err.Error()),
×
651
                )
×
652
                err = fmt.Errorf("error decoding return value for module %q: %w", call.Args.Name, err)
×
653
                span.SetStatus(codes.Error, err.Error())
×
654
                return returns, err
×
655
        }
×
656

657
        logger.DebugContext(
128✔
658
                ctx,
128✔
659
                "AnsibleExecute: returning result",
128✔
660
                telemetry.SlogJSON("return", returns),
128✔
661
        )
128✔
662
        span.SetStatus(codes.Ok, "")
128✔
663
        return returns, nil
128✔
664
}
665

666
func DisconnectAll(ctx context.Context) error {
1✔
667
        ctx, span := Tracer.Start(ctx, "mid/provider/executor.DisconnectAll", trace.WithAttributes(
1✔
668
                attribute.String("exec.strategy", "rpc"),
1✔
669
        ))
1✔
670
        defer span.End()
1✔
671
        logger := telemetry.LoggerFromContext(ctx)
1✔
672

1✔
673
        var multierr error
1✔
674

1✔
675
        for id, cs := range AgentPool.Items() {
2✔
676
                logger.DebugContext(ctx, fmt.Sprintf("DisconnectAll: disconnecting %d", id))
1✔
677
                cs.SetupAgentMutex.Lock()
1✔
678
                cs.CanConnectMutex.Lock()
1✔
679
                err := cs.Agent.Disconnect(ctx, true)
1✔
680
                multierr = errors.Join(multierr, err)
1✔
681
                cs.Agent = nil
1✔
682
                cs.Reachable = false
1✔
683
                cs.CanConnectMutex.Unlock()
1✔
684
                cs.SetupAgentMutex.Unlock()
1✔
685
                AgentPool.Delete(id)
1✔
686
                logger.DebugContext(ctx, fmt.Sprintf("DisconnectAll: disconnected %d", id), slog.Any("error", err))
1✔
687
        }
1✔
688

689
        logger.DebugContext(ctx, "DisconnectAll: finished disconnecting all", slog.Any("error", multierr))
1✔
690
        return multierr
1✔
691
}
692

693
func StageFile(
694
        ctx context.Context,
695
        connection midtypes.Connection,
696
        resourceConfig midtypes.ResourceConfig,
697
        f io.Reader,
698
) (string, error) {
13✔
699
        ctx, span := Tracer.Start(ctx, "mid/provider/executor.StageFile")
13✔
700
        defer span.End()
13✔
701
        logger := telemetry.LoggerFromContext(ctx).With()
13✔
702

13✔
703
        if connection.Host != nil {
26✔
704
                span.SetAttributes(
13✔
705
                        attribute.String("connection.host", *connection.Host),
13✔
706
                )
13✔
707
                logger = logger.With(
13✔
708
                        slog.String("connection.host", *connection.Host),
13✔
709
                )
13✔
710
        }
13✔
711

712
        cs, err := Acquire(ctx, connection, resourceConfig)
13✔
713
        if err != nil {
13✔
714
                span.SetStatus(codes.Error, err.Error())
×
715
                return "", err
×
716
        }
×
717
        defer cs.FinishedTask()
13✔
718

13✔
719
        err = cs.SetupAgent(ctx)
13✔
720
        if err != nil {
13✔
721
                span.SetStatus(codes.Error, err.Error())
×
722
                return "", err
×
723
        }
×
724

725
        logger.DebugContext(ctx, "StageFile: staging file")
13✔
726
        remotePath, err := midagent.StageFile(ctx, cs.Agent, f)
13✔
727
        span.SetAttributes(attribute.String("remote_path", remotePath))
13✔
728
        if err != nil {
13✔
729
                logger.ErrorContext(ctx, "StageFile: error staging file", slog.Any("error", err))
×
730
                span.SetStatus(codes.Error, err.Error())
×
731
                return remotePath, err
×
732
        }
×
733

734
        logger.DebugContext(ctx, "StageFile: finished staging file", slog.String("remote_path", remotePath))
13✔
735
        span.SetStatus(codes.Ok, "")
13✔
736
        return remotePath, nil
13✔
737
}
738

739
func ConnectionToSSHClientConfig(connection midtypes.Connection) (*ssh.ClientConfig, string, error) {
109✔
740
        sshConfig := &ssh.ClientConfig{}
109✔
741

109✔
742
        port := midtypes.DefaultConnectionPort
109✔
743
        if connection.Port != nil {
218✔
744
                port = int(*connection.Port)
109✔
745
        }
109✔
746

747
        if connection.Host == nil {
109✔
748
                return nil, "", errors.Join(ErrUnreachable, ErrHostUnset)
×
749
        }
×
750

751
        endpoint := net.JoinHostPort(*connection.Host, fmt.Sprintf("%d", port))
109✔
752

109✔
753
        sshConfig.User = midtypes.DefaultConnectionUser
109✔
754
        if connection.User == nil {
109✔
755
                current, err := user.Current()
×
756
                if err == nil {
×
757
                        sshConfig.User = current.Username
×
758
                }
×
759
        } else {
109✔
760
                sshConfig.User = *connection.User
109✔
761
        }
109✔
762

763
        sshConfig.Timeout = time.Second * time.Duration(midtypes.DefaultConnectionPerDialTimeout)
109✔
764
        if connection.PerDialTimeout != nil {
109✔
765
                sshConfig.Timeout = time.Second * time.Duration(*connection.PerDialTimeout)
×
766
        }
×
767

768
        if connection.HostKey != nil {
109✔
769
                publicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(*connection.HostKey))
×
770
                if err != nil {
×
771
                        return sshConfig, endpoint, fmt.Errorf("failed to parse host key: %w", err)
×
772
                }
×
773
                sshConfig.HostKeyCallback = ssh.FixedHostKey(publicKey)
×
774
                sshConfig.HostKeyAlgorithms = []string{publicKey.Type()}
×
775
        } else {
109✔
776
                sshConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey()
109✔
777
        }
109✔
778

779
        if connection.PrivateKey != nil {
110✔
780
                var signer ssh.Signer
1✔
781
                var err error
1✔
782
                if connection.PrivateKeyPassword != nil {
1✔
783
                        signer, err = ssh.ParsePrivateKeyWithPassphrase(
×
784
                                []byte(*connection.PrivateKey),
×
785
                                []byte(*connection.PrivateKeyPassword),
×
786
                        )
×
787
                } else {
1✔
788
                        signer, err = ssh.ParsePrivateKey([]byte(*connection.PrivateKey))
1✔
789
                }
1✔
790
                if err != nil {
1✔
791
                        return sshConfig, endpoint, err
×
792
                }
×
793
                sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(signer))
1✔
794
        }
795

796
        if connection.Password != nil {
217✔
797
                sshConfig.Auth = append(sshConfig.Auth, ssh.Password(*connection.Password))
108✔
798
                sshConfig.Auth = append(sshConfig.Auth, ssh.KeyboardInteractive(
108✔
799
                        func(user, instruction string, questions []string, echos []bool) ([]string, error) {
108✔
800
                                answers := make([]string, len(questions))
×
801
                                for i := range questions {
×
802
                                        answers[i] = *connection.Password
×
803
                                }
×
804
                                return answers, nil
×
805
                        },
806
                ))
807
        }
808

809
        sshAgent := false
109✔
810
        sshAgentSocketPath := os.Getenv("SSH_AUTH_SOCK")
109✔
811
        if connection.SSHAgentSocketPath != nil {
109✔
812
                sshAgent = true
×
813
                sshAgentSocketPath = *connection.SSHAgentSocketPath
×
814
        }
×
815
        if connection.SSHAgent != nil && *connection.SSHAgent {
109✔
816
                sshAgent = true
×
817
        }
×
818
        if sshAgent && sshAgentSocketPath != "" {
109✔
819
                agentConn, err := net.Dial("unix", sshAgentSocketPath)
×
820
                if err != nil {
×
821
                        return sshConfig, endpoint, err
×
822
                }
×
823
                sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeysCallback(agent.NewClient(agentConn).Signers))
×
824
        }
825

826
        return sshConfig, endpoint, nil
109✔
827
}
828

829
func DialWithRetry[T any](ctx context.Context, msg string, maxAttempts int, f func() (T, error)) (T, error) {
109✔
830
        ctx, span := Tracer.Start(ctx, "mid/provider/executor.DialWithRetry", trace.WithAttributes(
109✔
831
                attribute.String("exec.strategy", "rpc"),
109✔
832
        ))
109✔
833
        defer span.End()
109✔
834

109✔
835
        var userError error
109✔
836
        ok, data, err := retry.Until(ctx, retry.Acceptor{
109✔
837
                // TODO: make Delay and MaxDelay configurable
109✔
838
                Delay:    ptr.Of(time.Second),
109✔
839
                MaxDelay: ptr.Of(time.Minute),
109✔
840
                Accept: func(try int, _ time.Duration) (bool, any, error) {
218✔
841
                        _, subspan := Tracer.Start(ctx, "mid/provider/executor.DialWithRetry:Attempt", trace.WithAttributes(
109✔
842
                                attribute.Int("retry.attempt", try),
109✔
843
                        ))
109✔
844
                        defer subspan.End()
109✔
845
                        logger := telemetry.LoggerFromContext(ctx).With(
109✔
846
                                slog.Int("retry.attempt", try),
109✔
847
                                slog.Int("retry.max_attempts", maxAttempts),
109✔
848
                        )
109✔
849

109✔
850
                        logger.DebugContext(ctx, "DialWithRetry.Attempt: starting attempt")
109✔
851

109✔
852
                        var result T
109✔
853
                        result, userError = f()
109✔
854
                        if userError == nil {
218✔
855
                                logger.DebugContext(ctx, "DialWithRetry.Attempt: success")
109✔
856
                                subspan.SetStatus(codes.Ok, "")
109✔
857
                                return true, result, nil
109✔
858
                        }
109✔
859
                        dials := try + 1
1✔
860
                        if maxAttempts > -1 && dials > maxAttempts {
1✔
861
                                err := fmt.Errorf(
×
862
                                        "after %d failed attempts: %w",
×
863
                                        try,
×
864
                                        userError,
×
865
                                )
×
866
                                p.GetLogger(ctx).ErrorStatus(err.Error())
×
867
                                subspan.SetStatus(codes.Error, err.Error())
×
868
                                logger.ErrorContext(ctx, "DialWithRetry.Attempt: giving up", slog.Any("error", err))
×
869
                                return true, nil, err
×
870
                        }
×
871
                        var limit string
1✔
872
                        if maxAttempts == -1 {
1✔
873
                                limit = "inf"
×
874
                        } else {
1✔
875
                                limit = fmt.Sprintf("%d", maxAttempts)
1✔
876
                        }
1✔
877
                        msg := fmt.Sprintf(
1✔
878
                                "%s %d/%s failed: retrying",
1✔
879
                                msg,
1✔
880
                                dials,
1✔
881
                                limit,
1✔
882
                        )
1✔
883
                        subspan.SetStatus(codes.Error, msg)
1✔
884
                        p.GetLogger(ctx).InfoStatus(msg)
1✔
885
                        logger.DebugContext(ctx, fmt.Sprintf("DialWithRetry.Attempt: %s", msg))
1✔
886
                        return false, nil, nil
1✔
887
                },
888
        })
889
        if ok && err == nil {
218✔
890
                p.GetLogger(ctx).InfoStatusf("%s: success", msg)
109✔
891
                span.SetStatus(codes.Ok, "")
109✔
892
                return data.(T), nil
109✔
893
        }
109✔
894

895
        var t T
×
896
        if err == nil {
×
897
                err = ctx.Err()
×
898
        }
×
899
        if err != nil {
×
900
                span.SetStatus(codes.Error, err.Error())
×
901
                return t, err
×
902
        }
×
903

904
        span.SetStatus(codes.Ok, "")
×
905
        return t, nil
×
906
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc