Files
helmfile/pkg/helmexec/span_test.go
T
yxxheroandClaude ad81e4231e build: update Go to 1.27.1 and modernize the codebase (#2798)
Toolchain and CI:
- Bump go directive from 1.26.8 to 1.27.1 (go.mod)
- Use golang:1.27-alpine builder images in all Dockerfiles
- Bump golangci-lint to v2.13.2 (first release with go1.27 support)
- Add CI gate: `go fix -diff` fails when outdated Go patterns are
  detected (locally: `make check-modernize`)

Note: darwin binaries now require macOS 13 or later.

Lint fixes required by golangci-lint v2.13.2:
- goconst: ignore tests (all 436 findings were test-only; goconst
  got stricter since v2.12 and this option was added for it)
- openai.go: keep deprecated MaxTokens deliberately with a nolint
  rationale (max_tokens is the only form universally supported by
  OpenAI-compatible backends like One-API, LiteLLM, Ollama shim)
- state.go: drop always-nil flags param from appendChartVersionFlags
  (renamed to chartVersionFlags, unparam)

Modernization (go fix ./..., 62 files):
- interface{} -> any, maps.Copy, strings.SplitSeq, range-over-int,
  builtin min/max, slices.Contains/ContainsFunc/Sort, WaitGroup.Go,
  reflect.Type.Fields(), new(expr)
- exit_error.go: strings.Builder + fmt.Fprintf instead of string
  concatenation and WriteString(fmt.Sprintf(...)) (QF1012)
- chart_dependency.go: strings.CutLast for OCI dependency helpers

Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-16 13:29:02 +08:00

166 lines
6.0 KiB
Go

package helmexec
import (
"context"
"io"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
v1 "go.opentelemetry.io/proto/otlp/trace/v1"
"github.com/helmfile/helmfile/pkg/telemetry"
"github.com/helmfile/helmfile/pkg/telemetry/otlptest"
)
// TestShellRunnerExecSpansExported drives ShellRunner.Execute through a real
// OTLP export and asserts names, attributes, error status, and parent linkage
// to the command span (i.e. the runner spans nest, never orphan).
func TestShellRunnerExecSpansExported(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses the unix true/false binaries")
}
rec := otlptest.NewRecorder(t)
otlptest.SetupTelemetry(t, rec, "helmfile test")
shell := ShellRunner{
Logger: NewLogger(io.Discard, "warn"),
Ctx: telemetry.CommandContext(),
}
out, err := shell.Execute("true", nil, nil, false)
require.NoError(t, err)
assert.Empty(t, out)
// A failing command must surface its exit code and error status.
_, err = shell.Execute("false", nil, nil, false)
var exitErr ExitError
require.ErrorAs(t, err, &exitErr)
// Secret-bearing arguments must be redacted on the span.
_, err = shell.Execute("true", []string{"--set", "secret=1", "--set=also=2"}, nil, false)
require.NoError(t, err)
otlptest.ShutdownTelemetry(t)
spans := rec.Spans(t)
root := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool { return s.Name == "helmfile test" }, "command span")
success := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool {
cmd, ok := otlptest.AttrString(s, "exec.command")
return s.Name == "os.exec" && ok && cmd == "true" && !otlptest.HasAttr(s, "exec.redacted")
}, "success os.exec span")
if success.Status != nil {
assert.Equal(t, v1.Status_STATUS_CODE_UNSET, success.Status.Code, "success span must not be marked error")
}
failure := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool {
cmd, ok := otlptest.AttrString(s, "exec.command")
return ok && cmd == "false"
}, "failure os.exec span")
require.NotNil(t, failure.Status)
assert.Equal(t, v1.Status_STATUS_CODE_ERROR, failure.Status.Code)
assert.EqualValues(t, 1, spanAttr(t, failure, "exec.exit_code").Value.GetIntValue())
redactedSpan := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool { return otlptest.HasAttr(s, "exec.redacted") }, "redacted os.exec span")
assert.Equal(t, []string{"--set", redactedArg, "--set=" + redactedArg}, spanAttrStrings(t, redactedSpan, "exec.args"))
// Every os.exec span must be a child of the command span, proving the
// runner-span nesting (no orphan traces).
for _, s := range spans {
if s.Name == "os.exec" {
assert.Equal(t, root.TraceId, s.TraceId, "os.exec span must join the command trace")
assert.Equal(t, root.SpanId, s.ParentSpanId, "os.exec span must nest under the command span")
}
}
}
func spanAttr(t *testing.T, span *v1.Span, key string) *commonpb.KeyValue {
t.Helper()
for _, kv := range span.Attributes {
if kv.Key == key {
return kv
}
}
t.Fatalf("attribute %q not found on span %q", key, span.Name)
return nil
}
func spanAttrStrings(t *testing.T, span *v1.Span, key string) []string {
t.Helper()
values := spanAttr(t, span, key).Value.GetArrayValue().GetValues()
out := make([]string, 0, len(values))
for _, v := range values {
out = append(out, v.GetStringValue())
}
return out
}
// TestSpanAttachedContext pins the per-call context merge that lets helm
// subprocess spans nest under per-release spans WITHOUT overriding the
// runner's own cancellation context (the kubedog safety valve).
func TestSpanAttachedContext(t *testing.T) {
runnerCtx := t.Context()
spanCtx, span := noop.NewTracerProvider().Tracer("test").Start(context.Background(), "release")
merged := spanAttachedContext(runnerCtx, spanCtx)
// The merged context still cancels with the runner's context.
assert.Equal(t, runnerCtx.Done(), merged.Done(), "cancellation authority must stay with the runner context")
// ...and carries the caller's span for nesting.
assert.Equal(t, span, trace.SpanFromContext(merged), "the caller's span must be attached")
// A typed nil runner context (distinct from the nil literal, per
// staticcheck) falls back to the span context.
var nilRunnerCtx context.Context
assert.Equal(t, spanCtx, spanAttachedContext(nilRunnerCtx, spanCtx))
}
// TestMarkHelmRunnerCoversBothFunnels pins that the shared marker helper —
// used by BOTH execWithRunner and execStdIn — stamps value and pointer
// runners alike, so wrapper binaries stay classified as helm operations on
// stdin-based invocations (registry login, repo add) too.
func TestMarkHelmRunnerCoversBothFunnels(t *testing.T) {
ptr := &ShellRunner{}
if v, ok := markHelmRunner(ptr).(*ShellRunner).Ctx.Value(helmExecMarker{}).(bool); !ok || !v {
t.Error("pointer runner must carry the helm marker")
}
val := ShellRunner{}
markedValue, ok := markHelmRunner(val).(ShellRunner)
if !ok {
t.Fatal("value runner must stay a value runner")
}
if v, ok := markedValue.Ctx.Value(helmExecMarker{}).(bool); !ok || !v {
t.Error("value runner must carry the helm marker")
}
fake := &mockRunner{}
assert.Same(t, fake, markHelmRunner(fake), "non-ShellRunner runners pass through unchanged")
}
// TestValueRunnerClassification pins that ShellRunner values (which satisfy
// Runner via value receivers) receive the helm marker and span attachment
// exactly like pointer runners.
func TestValueRunnerClassification(t *testing.T) {
spanCtx, span := noop.NewTracerProvider().Tracer("test").Start(context.Background(), "release")
// marker stamping
valueRunner := ShellRunner{}
marked := markHelmExec(valueRunner.Ctx)
if v, ok := marked.Value(helmExecMarker{}).(bool); !ok || !v {
t.Fatal("value runner ctx must carry the helm marker")
}
// span attachment via the same path execWithContext uses for values
merged := spanAttachedContext(valueRunner.Ctx, spanCtx)
assert.Equal(t, span, trace.SpanFromContext(merged))
}