mirror of
https://github.com/holos-run/holos.git
synced 2026-03-20 09:15:02 +00:00
Switch holos to use v1alpha3 so we can establish more of the CUE structures in the documented API using Go structs.
171 lines
5.8 KiB
Go
171 lines
5.8 KiB
Go
package render
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/holos-run/holos"
|
|
core "github.com/holos-run/holos/api/core/v1alpha3"
|
|
"github.com/holos-run/holos/internal/errors"
|
|
"github.com/holos-run/holos/internal/server/middleware/logger"
|
|
"github.com/holos-run/holos/internal/util"
|
|
)
|
|
|
|
type HelmChart struct {
|
|
Component core.HelmChart `json:"component"`
|
|
}
|
|
|
|
func (hc *HelmChart) Render(ctx context.Context, path holos.InstancePath) (*Result, error) {
|
|
if hc == nil {
|
|
return nil, nil
|
|
}
|
|
result := NewResult(hc.Component.Component)
|
|
if err := hc.helm(ctx, result, path); err != nil {
|
|
return nil, err
|
|
}
|
|
result.addObjectMap(ctx, hc.Component.APIObjectMap)
|
|
if err := result.kustomize(ctx); err != nil {
|
|
return nil, errors.Wrap(fmt.Errorf("could not kustomize: %w", err))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// runHelm provides the values produced by CUE to helm template and returns
|
|
// the rendered kubernetes api objects in the result.
|
|
func (hc *HelmChart) helm(ctx context.Context, r *Result, path holos.InstancePath) error {
|
|
log := logger.FromContext(ctx).With("chart", hc.Component.Chart.Name)
|
|
if hc.Component.Chart.Name == "" {
|
|
log.WarnContext(ctx, "skipping helm: no chart name specified, use a different component type")
|
|
return nil
|
|
}
|
|
|
|
cachedChartPath := filepath.Join(string(path), core.ChartDir, filepath.Base(hc.Component.Chart.Name))
|
|
if isNotExist(cachedChartPath) {
|
|
// Add repositories
|
|
repo := hc.Component.Chart.Repository
|
|
if repo.URL != "" {
|
|
out, err := util.RunCmd(ctx, "helm", "repo", "add", repo.Name, repo.URL)
|
|
if err != nil {
|
|
log.ErrorContext(ctx, "could not run helm", "stderr", out.Stderr.String(), "stdout", out.Stdout.String())
|
|
return errors.Wrap(fmt.Errorf("could not run helm repo add: %w", err))
|
|
}
|
|
// Update repository
|
|
out, err = util.RunCmd(ctx, "helm", "repo", "update", repo.Name)
|
|
if err != nil {
|
|
log.ErrorContext(ctx, "could not run helm", "stderr", out.Stderr.String(), "stdout", out.Stdout.String())
|
|
return errors.Wrap(fmt.Errorf("could not run helm repo update: %w", err))
|
|
}
|
|
} else {
|
|
log.DebugContext(ctx, "no chart repository url proceeding assuming oci chart")
|
|
}
|
|
|
|
// Cache the chart
|
|
if err := cacheChart(ctx, path, core.ChartDir, hc.Component.Chart); err != nil {
|
|
return fmt.Errorf("could not cache chart: %w", err)
|
|
}
|
|
}
|
|
|
|
// Write values file
|
|
tempDir, err := os.MkdirTemp("", "holos")
|
|
if err != nil {
|
|
return errors.Wrap(fmt.Errorf("could not make temp dir: %w", err))
|
|
}
|
|
defer util.Remove(ctx, tempDir)
|
|
|
|
valuesPath := filepath.Join(tempDir, "values.yaml")
|
|
if err := os.WriteFile(valuesPath, []byte(hc.Component.ValuesContent), 0644); err != nil {
|
|
return errors.Wrap(fmt.Errorf("could not write values: %w", err))
|
|
}
|
|
log.DebugContext(ctx, "helm: wrote values", "path", valuesPath, "bytes", len(hc.Component.ValuesContent))
|
|
|
|
// Run charts
|
|
chart := hc.Component.Chart
|
|
args := []string{"template"}
|
|
if !hc.Component.EnableHooks {
|
|
args = append(args, "--no-hooks")
|
|
}
|
|
namespace := hc.Component.Metadata.Namespace
|
|
args = append(args, "--include-crds", "--values", valuesPath, "--namespace", namespace, "--kubeconfig", "/dev/null", "--version", chart.Version, chart.Release, cachedChartPath)
|
|
helmOut, err := util.RunCmd(ctx, "helm", args...)
|
|
if err != nil {
|
|
stderr := helmOut.Stderr.String()
|
|
lines := strings.Split(stderr, "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "Error:") {
|
|
err = fmt.Errorf("%s: %w", line, err)
|
|
}
|
|
}
|
|
return errors.Wrap(fmt.Errorf("could not run helm template: %w", err))
|
|
}
|
|
|
|
r.accumulatedOutput = helmOut.Stdout.String()
|
|
|
|
return nil
|
|
}
|
|
|
|
// cacheChart stores a cached copy of Chart in the chart subdirectory of path.
|
|
//
|
|
// It is assumed that the only method responsible for writing to chartDir is
|
|
// cacheChart itself.
|
|
//
|
|
// This relies on the atomicity of moving temporary directories into place on
|
|
// the same filesystem via os.Rename. If a syscall.EEXIST error occurs during
|
|
// renaming, it indicates that the cached chart already exists, which is an
|
|
// expected scenario when this function is called concurrently.
|
|
func cacheChart(ctx context.Context, path holos.InstancePath, chartDir string, chart core.Chart) error {
|
|
log := logger.FromContext(ctx)
|
|
|
|
cacheTemp, err := os.MkdirTemp(string(path), chartDir)
|
|
if err != nil {
|
|
return errors.Wrap(fmt.Errorf("could not make temp dir: %w", err))
|
|
}
|
|
defer util.Remove(ctx, cacheTemp)
|
|
|
|
chartName := chart.Name
|
|
if chart.Repository.Name != "" {
|
|
chartName = fmt.Sprintf("%s/%s", chart.Repository.Name, chart.Name)
|
|
}
|
|
helmOut, err := util.RunCmd(ctx, "helm", "pull", "--destination", cacheTemp, "--untar=true", "--version", chart.Version, chartName)
|
|
if err != nil {
|
|
return errors.Wrap(fmt.Errorf("could not run helm pull: %w", err))
|
|
}
|
|
log.Debug("helm pull", "stdout", helmOut.Stdout, "stderr", helmOut.Stderr)
|
|
|
|
cachePath := filepath.Join(string(path), chartDir)
|
|
|
|
if err := os.MkdirAll(cachePath, 0777); err != nil {
|
|
return errors.Wrap(fmt.Errorf("could not mkdir: %w", err))
|
|
}
|
|
|
|
items, err := os.ReadDir(cacheTemp)
|
|
if err != nil {
|
|
return errors.Wrap(fmt.Errorf("could not read directory: %w", err))
|
|
}
|
|
|
|
for _, item := range items {
|
|
src := filepath.Join(cacheTemp, item.Name())
|
|
dst := filepath.Join(cachePath, item.Name())
|
|
log.DebugContext(ctx, "rename", "src", src, "dst", dst)
|
|
if err := os.Rename(src, dst); err != nil {
|
|
var linkErr *os.LinkError
|
|
if errors.As(err, &linkErr) && errors.Is(linkErr.Err, syscall.EEXIST) {
|
|
log.DebugContext(ctx, "cache already exists", "chart", chart.Name, "chart_version", chart.Version, "path", cachePath)
|
|
} else {
|
|
return errors.Wrap(fmt.Errorf("could not rename: %w", err))
|
|
}
|
|
}
|
|
}
|
|
|
|
log.InfoContext(ctx, "cached", "chart", chart.Name, "chart_version", chart.Version, "path", cachePath)
|
|
|
|
return nil
|
|
}
|
|
func isNotExist(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return os.IsNotExist(err)
|
|
}
|