Ctrl K
ComunidadGuías…

internal/helm/actions.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package helm

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/sophotechlabs/spinoza/internal/api"
	"github.com/sophotechlabs/spinoza/internal/auth"
	"github.com/sophotechlabs/spinoza/internal/commandbuffer"
)

const (
	DefaultBinary  = "helm"
	actionTimeout  = 5 * time.Minute
	maxHelmOutput  = 16 << 20
	maxHelmError   = 64 << 10
	driverEnv      = "HELM_DRIVER"
	ActionRollback = "rollback"
	ActionUninstal = "uninstall"
)

var nameFormat = regexp.MustCompile(`^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$`)

var ErrNoHelmBinary = errors.New("helm was not found on PATH")

type Runner interface {
	Run(ctx context.Context, args, env []string) (string, error)
	Available() error
}

type helmRunner struct {
	binary      string
	outputLimit int
	errorLimit  int
}

func NewRunner(binary string) Runner {
	if binary == "" {
		binary = DefaultBinary
	}
	return &helmRunner{binary: binary, outputLimit: maxHelmOutput, errorLimit: maxHelmError}
}

func (h *helmRunner) Available() error {
	_, err := exec.LookPath(h.binary)
	if err != nil {
		return fmt.Errorf("%w: %s", ErrNoHelmBinary, h.binary)
	}
	return nil
}

func (h *helmRunner) Run(ctx context.Context, args, env []string) (string, error) {
	//nolint:gosec // the binary is a flag, every argument is validated below and passed as argv, never through a shell
	command := exec.CommandContext(ctx, h.binary, args...)
	stdout := commandbuffer.Head(h.outputLimit)
	stderr := commandbuffer.Tail(h.errorLimit)
	command.Stdout = stdout
	command.Stderr = stderr
	command.Stdin = nil
	command.Env = helmEnvironment(env)

	err := command.Run()
	if stdout.Exceeded() {
		return "", fmt.Errorf("helm output exceeded %d bytes", h.outputLimit)
	}
	if err == nil {
		return strings.TrimSpace(stdout.String()), nil
	}
	if _, missing := errors.AsType[*exec.Error](err); missing {
		return "", fmt.Errorf("%w: %s", ErrNoHelmBinary, h.binary)
	}
	message := firstMeaningful(stderr.String())
	if stderr.Exceeded() {
		return "", fmt.Errorf("helm error output exceeded %d bytes: %s", h.errorLimit, message)
	}
	if message == "" {
		return "", err
	}
	return "", errors.New(message)
}

func helmEnvironment(extra []string) []string {
	held := map[string]string{}
	for _, entry := range os.Environ() {
		key, _, ok := strings.Cut(entry, "=")
		if !ok {
			continue
		}
		if !allowedHelmEnvironment(key) {
			continue
		}
		held[key] = entry
	}
	for _, entry := range extra {
		key, _, ok := strings.Cut(entry, "=")
		if !ok {
			continue
		}
		held[key] = entry
	}
	keys := make([]string, 0, len(held))
	for key := range held {
		keys = append(keys, key)
	}
	slices.Sort(keys)
	out := make([]string, 0, len(keys))
	for _, key := range keys {
		out = append(out, held[key])
	}
	return out
}

var helmEnvironmentPrefixes = []string{"HELM_", "XDG_", "AWS_", "CLOUDSDK_", "GOOGLE_", "AZURE_"}

func allowedHelmEnvironment(key string) bool {
	for _, prefix := range helmEnvironmentPrefixes {
		if strings.HasPrefix(key, prefix) {
			return true
		}
	}
	switch key {
	case "HOME", "KUBECONFIG", "PATH", "TMPDIR",
		"SSL_CERT_DIR", "SSL_CERT_FILE",
		"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
		"http_proxy", "https_proxy", "no_proxy":
		return true
	default:
		return false
	}
}

func firstMeaningful(stderr string) string {
	lines := strings.Split(strings.TrimSpace(stderr), "\n")
	kept := make([]string, 0, len(lines))
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" {
			continue
		}
		kept = append(kept, trimmed)
	}
	if len(kept) == 0 {
		return ""
	}
	return strings.Join(kept, "; ")
}

func (s *Service) Support() api.HelmSupport {
	support := api.HelmSupport{Binary: DefaultBinary}
	if s.runner == nil {
		support.Reason = "helm actions are not wired up"
		return support
	}
	err := s.runner.Available()
	if err != nil {
		support.Reason = err.Error()
		return support
	}
	support.Available = true
	return support
}

func (s *Service) Rollback(ctx context.Context, namespace, name string, revision int64) (api.HelmActionResult, error) {
	err := s.admits(namespace, name)
	if err != nil {
		return api.HelmActionResult{}, err
	}
	if revision < 1 {
		return api.HelmActionResult{}, errors.New("a rollback needs the revision to go back to")
	}
	driver, driverErr := s.driverFor(ctx, namespace, name)
	if driverErr != nil {
		return api.HelmActionResult{}, driverErr
	}
	args := s.args(ctx, "rollback", name, strconv.FormatInt(revision, 10), "--namespace", namespace)
	out, runErr := s.run(ctx, args, driver)
	if runErr != nil {
		return api.HelmActionResult{}, runErr
	}
	return api.HelmActionResult{
		Action:   ActionRollback,
		Message:  orDefault(out, fmt.Sprintf("rolled %s back to revision %d", name, revision)),
		Revision: revision,
	}, nil
}

func (s *Service) Uninstall(ctx context.Context, namespace, name string) (api.HelmActionResult, error) {
	err := s.admits(namespace, name)
	if err != nil {
		return api.HelmActionResult{}, err
	}
	driver, driverErr := s.driverFor(ctx, namespace, name)
	if driverErr != nil {
		return api.HelmActionResult{}, driverErr
	}
	args := s.args(ctx, "uninstall", name, "--namespace", namespace)
	out, runErr := s.run(ctx, args, driver)
	if runErr != nil {
		return api.HelmActionResult{}, runErr
	}
	return api.HelmActionResult{
		Action:  ActionUninstal,
		Message: orDefault(out, fmt.Sprintf("uninstalled %s from %s", name, namespace)),
	}, nil
}

func (s *Service) admits(namespace, name string) error {
	if s.runner == nil {
		return errors.New("helm actions are not wired up")
	}
	if !nameFormat.MatchString(namespace) || !nameFormat.MatchString(name) {
		return errors.New("namespace and release must be valid kubernetes names")
	}
	return nil
}

func (s *Service) args(ctx context.Context, rest ...string) []string {
	args := append([]string{}, rest...)
	if s.kubeRef.Name != "" {
		args = append(args, "--kube-context", s.kubeRef.Name)
	}
	if s.kubeRef.Kubeconfig != "" {
		args = append(args, "--kubeconfig", s.kubeRef.Kubeconfig)
	}
	return append(args, auth.HelmFlags(ctx)...)
}

func (s *Service) run(ctx context.Context, args []string, driver string) (string, error) {
	bounded, cancel := context.WithTimeout(ctx, actionTimeout)
	defer cancel()
	env := []string{}
	if driver == DriverConfigMap {
		env = append(env, driverEnv+"="+DriverConfigMap)
	}
	return s.runner.Run(bounded, args, env)
}

func (s *Service) driverFor(ctx context.Context, namespace, name string) (string, error) {
	bounded, cancel := context.WithTimeout(ctx, listTimeout)
	defer cancel()
	if s.meta == nil {
		revisions, err := revisionsIn(bounded, s.cs, namespace, name)
		if err != nil {
			return "", err
		}
		if len(revisions) == 0 {
			return "", fmt.Errorf("%w: %s/%s", ErrNoRelease, namespace, name)
		}
		latest := revisions[0]
		for _, revision := range revisions[1:] {
			if revision.revision > latest.revision {
				latest = revision
				continue
			}
			if revision.revision == latest.revision {
				return "", fmt.Errorf("%w: revision %d", errAmbiguousRevision, revision.revision)
			}
		}
		return latest.driver, nil
	}
	latest, err := s.latestRef(bounded, namespace, name)
	if err != nil {
		return "", err
	}
	return latest.driver, nil
}

func orDefault(out, fallback string) string {
	if out == "" {
		return fallback
	}
	return out
}