Skip to content

Commit 76e5dd5

Browse files
authored
fix(wfctl): preserve Docker resolver failure evidence (#1067)
* fix(wfctl): preserve Docker resolver failure evidence * fix(wfctl): use tagged switch for resolver outcomes
1 parent 38b877b commit 76e5dd5

3 files changed

Lines changed: 308 additions & 6 deletions

File tree

‎cmd/wfctl/pipeline_docker_unix.go‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,13 @@ type pipelineDockerProcessGroupKey struct{}
6161

6262
var pipelineDockerTLSNames = [3]string{"ca.pem", "cert.pem", "key.pem"}
6363

64-
func resolvePipelineDockerClient(ctx context.Context) (*pipelineDockerClient, error) {
64+
func resolvePipelineDockerClient(ctx context.Context) (_ *pipelineDockerClient, resolveErr error) {
65+
phase := "docker_resolve_cli"
66+
defer func() {
67+
if resolveErr != nil {
68+
pipelineRecordDiagnosticFromContext(ctx).fail(phase)
69+
}
70+
}()
6571
// Only discovery receives a snapshot of Docker's transport-selection env.
6672
// All daemon calls use a private empty config, explicit host/TLS flags, and
6773
// a minimal env, avoiding auth helpers, config proxies, and context changes.
@@ -78,10 +84,12 @@ func resolvePipelineDockerClient(ctx context.Context) (*pipelineDockerClient, er
7884
if err != nil {
7985
return nil, fmt.Errorf("pipeline Docker CLI unavailable")
8086
}
87+
phase = "docker_resolve_home"
8188
home := snapshot["HOME"]
8289
if home == "" {
8390
return nil, fmt.Errorf("pipeline Docker home unavailable")
8491
}
92+
phase = "docker_resolve_config"
8593
config := snapshot["DOCKER_CONFIG"]
8694
if config == "" {
8795
config = filepath.Join(home, ".docker")
@@ -97,14 +105,17 @@ func resolvePipelineDockerClient(ctx context.Context) (*pipelineDockerClient, er
97105
discoveryEnv = append(discoveryEnv, key+"="+value)
98106
}
99107
}
100-
out, err := runPipelineDockerCLI(ctx, binary, discoveryEnv, pipelineDockerCallTimeout, "--config", config, "context", "inspect")
108+
phase = "docker_resolve_context"
109+
out, err := runPipelineDockerCLI(pipelineDockerResolutionContext(ctx, "resolve_context"), binary, discoveryEnv, pipelineDockerCallTimeout, "--config", config, "context", "inspect")
101110
if err != nil {
102111
return nil, fmt.Errorf("pipeline Docker context resolution failed")
103112
}
113+
phase = "docker_resolve_context_metadata"
104114
var contexts []pipelineDockerContext
105115
if json.Unmarshal([]byte(out.Stdout), &contexts) != nil || len(contexts) != 1 {
106116
return nil, fmt.Errorf("pipeline Docker context metadata invalid")
107117
}
118+
phase = "docker_resolve_endpoint"
108119
metadata := contexts[0]
109120
endpoint, ok := metadata.Endpoints["docker"]
110121
if !ok || metadata.Name == "" {
@@ -113,6 +124,7 @@ func resolvePipelineDockerClient(ctx context.Context) (*pipelineDockerClient, er
113124
if err := validatePipelineDockerEndpoint(endpoint.Host); err != nil {
114125
return nil, err
115126
}
127+
phase = "docker_resolve_tls"
116128
var material [3][]byte
117129
tlsEnabled := false
118130
verifyTLS := false
@@ -153,6 +165,7 @@ func resolvePipelineDockerClient(ctx context.Context) (*pipelineDockerClient, er
153165
if (len(material[1]) == 0) != (len(material[2]) == 0) {
154166
return nil, fmt.Errorf("pipeline Docker TLS client material incomplete")
155167
}
168+
phase = "docker_resolve_private_config"
156169
dir, err := os.MkdirTemp("", "wfctl-pipeline-docker-")
157170
if err != nil {
158171
return nil, fmt.Errorf("pipeline Docker private config unavailable")
@@ -182,7 +195,8 @@ func resolvePipelineDockerClient(ctx context.Context) (*pipelineDockerClient, er
182195
c.args = append(c.args, flag+"="+path)
183196
}
184197
}
185-
serverID, err := c.serverID(ctx)
198+
phase = "docker_resolve_identity_metadata"
199+
serverID, err := c.serverID(pipelineDockerResolutionContext(ctx, "resolve_identity"))
186200
if err != nil {
187201
return nil, err
188202
}
@@ -426,13 +440,23 @@ func runPipelineDockerCLI(ctx context.Context, binary string, env []string, time
426440
if cmd.ProcessState != nil {
427441
result.ExitCode = cmd.ProcessState.ExitCode()
428442
}
429-
if ctx.Err() != nil {
443+
if contextErr := ctx.Err(); contextErr != nil {
444+
outcome := "command"
445+
switch contextErr {
446+
case context.DeadlineExceeded:
447+
outcome = "deadline"
448+
case context.Canceled:
449+
outcome = "cancelled"
450+
}
451+
observePipelineDockerResolutionFailure(ctx, outcome, result.ExitCode)
430452
return result, fmt.Errorf("pipeline Docker operation canceled or timed out")
431453
}
432454
if stdout.overflow || stderr.overflow {
455+
observePipelineDockerResolutionFailure(ctx, "capture", result.ExitCode)
433456
return result, fmt.Errorf("pipeline Docker output exceeded capture limit")
434457
}
435458
if err != nil {
459+
observePipelineDockerResolutionFailure(ctx, "command", result.ExitCode)
436460
return result, fmt.Errorf("pipeline Docker operation failed")
437461
}
438462
return result, nil

‎cmd/wfctl/pipeline_record_diagnostic_unix.go‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ var pipelineRecordDiagnosticPhases = map[string]bool{
4343
"container_create": true, "container_id": true, "container_attach": true,
4444
"container_wait": true, "container_status": true, "container_command": true,
4545
"cleanup": true, "pipeline_result": true, "record_write": true,
46+
"docker_resolve_cli": true, "docker_resolve_home": true, "docker_resolve_config": true,
47+
"docker_resolve_context": true, "docker_resolve_context_metadata": true,
48+
"docker_resolve_endpoint": true, "docker_resolve_tls": true,
49+
"docker_resolve_private_config": true, "docker_resolve_identity_metadata": true,
50+
"docker_resolve_context_cancelled": true, "docker_resolve_context_deadline": true,
51+
"docker_resolve_context_capture": true, "docker_resolve_context_command": true,
52+
"docker_resolve_identity_cancelled": true, "docker_resolve_identity_deadline": true,
53+
"docker_resolve_identity_capture": true, "docker_resolve_identity_command": true,
4654
}
4755

4856
type pipelineRecordDiagnostic struct {
@@ -63,6 +71,54 @@ func pipelineRecordDiagnosticFromContext(ctx context.Context) *pipelineRecordDia
6371
return diagnostic
6472
}
6573

74+
// The private tag applies only to the two initial resolver calls. Subsequent
75+
// server-ID checks and successful resolution never set operation/status fields.
76+
type pipelineDockerResolutionKey struct{}
77+
78+
func pipelineDockerResolutionContext(ctx context.Context, operation string) context.Context {
79+
if pipelineRecordDiagnosticFromContext(ctx) == nil {
80+
return ctx
81+
}
82+
switch operation {
83+
case "resolve_context", "resolve_identity":
84+
return context.WithValue(ctx, pipelineDockerResolutionKey{}, operation)
85+
default:
86+
return ctx
87+
}
88+
}
89+
90+
func observePipelineDockerResolutionFailure(ctx context.Context, outcome string, exit int) {
91+
diagnostic := pipelineRecordDiagnosticFromContext(ctx)
92+
if diagnostic == nil {
93+
return
94+
}
95+
operation, _ := ctx.Value(pipelineDockerResolutionKey{}).(string)
96+
var stage string
97+
switch operation {
98+
case "resolve_context":
99+
stage = "context"
100+
case "resolve_identity":
101+
stage = "identity"
102+
default:
103+
return
104+
}
105+
switch outcome {
106+
case "cancelled", "deadline", "capture", "command":
107+
default:
108+
return
109+
}
110+
diagnostic.mu.Lock()
111+
defer diagnostic.mu.Unlock()
112+
if diagnostic.report.Phase == "" {
113+
// Freeze the whole observation together; later cleanup or operations
114+
// cannot mix their status with the first resolver failure.
115+
diagnostic.report.Phase = "docker_resolve_" + stage + "_" + outcome
116+
diagnostic.report.Operation = operation
117+
diagnostic.report.DockerExit = pipelineRecordDiagnosticExit(exit)
118+
diagnostic.report.ContainerExit = -1
119+
}
120+
}
121+
66122
func (d *pipelineRecordDiagnostic) fail(phase string) {
67123
if d == nil {
68124
return

0 commit comments

Comments
 (0)