mirror of
https://github.com/helmfile/helmfile.git
synced 2026-09-30 09:41:47 +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>
168 lines
5.8 KiB
Go
168 lines
5.8 KiB
Go
package helmexec
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"github.com/helmfile/helmfile/pkg/telemetry"
|
|
)
|
|
|
|
// startExecSpan starts one span per external process started by helmfile
|
|
// (helm invocations, hooks, helmfile plugins). It is effectively free when
|
|
// telemetry is disabled: telemetry.Tracer then returns the OTel no-op tracer.
|
|
// The returned context derives from ctx (or Background when nil) and may be
|
|
// used for the subprocess itself without changing cancellation semantics.
|
|
// markHelmExec returns ctx (or Background when nil) stamped with the
|
|
// helm-invocation marker used for span classification.
|
|
func markHelmExec(ctx context.Context) context.Context {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
return context.WithValue(ctx, helmExecMarker{}, true)
|
|
}
|
|
|
|
// withRunnerCtx returns a runner (value or pointer ShellRunner form) whose
|
|
// context is transformed by f; non-ShellRunner runners pass through
|
|
// unchanged. ShellRunner has value receivers, so both forms satisfy Runner.
|
|
func withRunnerCtx(runner Runner, f func(context.Context) context.Context) Runner {
|
|
switch shell := runner.(type) {
|
|
case *ShellRunner:
|
|
clone := *shell
|
|
clone.Ctx = f(clone.Ctx)
|
|
return &clone
|
|
case ShellRunner:
|
|
clone := shell
|
|
clone.Ctx = f(clone.Ctx)
|
|
return clone
|
|
default:
|
|
return runner
|
|
}
|
|
}
|
|
|
|
// markHelmRunner returns a runner whose context carries the helm-invocation
|
|
// marker, so span classification and the helm duration metric do not rely on
|
|
// the executable basename.
|
|
func markHelmRunner(runner Runner) Runner {
|
|
return withRunnerCtx(runner, markHelmExec)
|
|
}
|
|
|
|
// spanAttachedContext returns a context that keeps runnerCtx's cancellation
|
|
// chain but carries the span from spanCtx, so the subprocess span nests under
|
|
// the caller's span while the subprocess itself stays governed by the
|
|
// runner's own context (e.g. the kubedog safety valve). A nil runnerCtx falls
|
|
// back to spanCtx.
|
|
func spanAttachedContext(runnerCtx, spanCtx context.Context) context.Context {
|
|
if runnerCtx == nil {
|
|
return spanCtx
|
|
}
|
|
return trace.ContextWithSpan(runnerCtx, trace.SpanFromContext(spanCtx))
|
|
}
|
|
|
|
// helmExecMarker marks contexts of invocations made through the execer
|
|
// funnel, so span classification is authoritative even for wrapper binaries
|
|
// whose name does not start with "helm".
|
|
type helmExecMarker struct{}
|
|
|
|
func startExecSpan(ctx context.Context, cmd string, args []string) (context.Context, trace.Span, bool) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
name, attrs := classifyExec(ctx, cmd, args)
|
|
ctx, span := telemetry.Tracer(telemetry.ScopeHelm).Start(ctx, name, trace.WithAttributes(attrs...))
|
|
return ctx, span, name == helmExecSpanName
|
|
}
|
|
|
|
// classifyExec builds the span name and attributes for one subprocess.
|
|
// Secret-bearing arguments are always redacted with the strict profile: span
|
|
// visibility must be at least as redacted as error messages (see redact.go).
|
|
// helmExecSpanName is the single source of truth for what counts as a helm
|
|
// invocation: the span name drives both classification and the helm duration
|
|
// metric gate.
|
|
const helmExecSpanName = "helm.exec"
|
|
|
|
func classifyExec(ctx context.Context, cmd string, args []string) (string, []attribute.KeyValue) {
|
|
base := filepath.Base(cmd)
|
|
isHelm := isHelmBinary(base)
|
|
if marked, ok := ctx.Value(helmExecMarker{}).(bool); ok {
|
|
isHelm = marked
|
|
}
|
|
attrs := []attribute.KeyValue{
|
|
attribute.String("exec.command", base),
|
|
}
|
|
|
|
redacted := RedactArgs(args, RedactionStrict)
|
|
for i, a := range redacted {
|
|
// Positional arguments can be chart/repository URLs with embedded
|
|
// credentials (AddRepo, RegistryLogin, OCI and go-getter refs).
|
|
redacted[i] = RedactedRef(a)
|
|
}
|
|
attrs = append(attrs, attribute.StringSlice("exec.args", redacted))
|
|
if !equalArgs(args, redacted) {
|
|
attrs = append(attrs, attribute.Bool("exec.redacted", true))
|
|
}
|
|
|
|
if isHelm {
|
|
if sub := helmSubcommand(args); sub != "" {
|
|
attrs = append(attrs, attribute.String("helm.subcommand", sub))
|
|
}
|
|
return helmExecSpanName, attrs
|
|
}
|
|
return "os.exec", attrs
|
|
}
|
|
|
|
// isHelmBinary is the FALLBACK classifier: a base name starting with "helm"
|
|
// ("helm", "helm3", "helm-dev") for processes started outside the execer
|
|
// funnels (e.g. the version probe in helmexec.New). Invocations through the
|
|
// funnels are classified authoritatively by the helmExecMarker, so wrapper
|
|
// --helm-binary names classify correctly too.
|
|
func isHelmBinary(base string) bool {
|
|
return strings.HasPrefix(base, "helm")
|
|
}
|
|
|
|
// helmSubcommand returns the best-effort helm subcommand ("upgrade",
|
|
// "repo", ...) from raw args. Bare flags are assumed to consume the next
|
|
// argument as their value (true for the global --kube-context/--kubeconfig
|
|
// helmfile prepends); a rare bare boolean flag before the subcommand can
|
|
// misattribute the value — cosmetic only.
|
|
func helmSubcommand(args []string) string {
|
|
skipValue := false
|
|
for _, a := range args {
|
|
if skipValue {
|
|
skipValue = false
|
|
continue
|
|
}
|
|
if strings.HasPrefix(a, "-") {
|
|
skipValue = !strings.Contains(a, "=")
|
|
continue
|
|
}
|
|
return a
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// finishExecSpan records a finished process's outcome on its span and, for
|
|
// helm invocations (as classified by startExecSpan, wrapper binaries
|
|
// included), the helmfile.helm.exec.duration metric.
|
|
func finishExecSpan(span trace.Span, isHelm bool, args []string, start time.Time, err error) {
|
|
if isHelm {
|
|
telemetry.RecordHelmExecDuration(time.Since(start).Seconds(), helmSubcommand(args), err == nil)
|
|
}
|
|
if err == nil {
|
|
return
|
|
}
|
|
if exitErr, ok := errors.AsType[ExitError](err); ok {
|
|
span.SetAttributes(attribute.Int("exec.exit_code", exitErr.ExitStatus()))
|
|
}
|
|
// The raw error may embed command arguments and subprocess output; keep
|
|
// the span description generic (the exit code is an attribute).
|
|
span.SetStatus(codes.Error, "command failed")
|
|
}
|