internal/helm/actions.go
Rama main · réplica de lectura del repositorio original.
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
}