mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-01 00:26:35 +02:00
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>
166 lines
6.0 KiB
Go
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))
|
|
}
|