github.com/helmwave/helmwave@v0.36.4-0.20240509190856-b35563eba4c6/pkg/release/values.go (about)

     1  package release
     2  
     3  import (
     4  	"context"
     5  	"crypto"
     6  	_ "crypto/md5" // for crypto.MD5.New to work
     7  	"encoding/hex"
     8  	"errors"
     9  	"fmt"
    10  	"path/filepath"
    11  	"slices"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/helmwave/helmwave/pkg/helper"
    17  	"github.com/helmwave/helmwave/pkg/parallel"
    18  	"github.com/helmwave/helmwave/pkg/release/uniqname"
    19  	"github.com/helmwave/helmwave/pkg/template"
    20  	"github.com/invopop/jsonschema"
    21  	log "github.com/sirupsen/logrus"
    22  	"github.com/stoewer/go-strcase"
    23  	"gopkg.in/yaml.v3"
    24  )
    25  
    26  // ValuesReference is used to match source values file path and temporary.
    27  //
    28  //nolint:lll
    29  type ValuesReference struct {
    30  	Src            string `yaml:"src" json:"src" jsonschema:"required,description=Source of values. Can be local path or HTTP URL"`
    31  	Dst            string `yaml:"dst" json:"dst" jsonschema:"readOnly"`
    32  	DelimiterLeft  string `yaml:"delimiter_left,omitempty" json:"delimiter_left,omitempty"  jsonschema:"Set left delimiter for template engine,default={{"`
    33  	DelimiterRight string `yaml:"delimiter_right,omitempty" json:"delimiter_right,omitempty" jsonschema:"Set right delimiter for template engine,default=}}"`
    34  	Renderer       string `yaml:"renderer" json:"renderer" jsonschema:"description=How to render the file,enum=sprig,enum=gomplate,enum=copy,enum=sops"`
    35  	Strict         bool   `yaml:"strict" json:"strict" jsonschema:"description=Whether to fail if values is not found,default=false"`
    36  }
    37  
    38  func (v *ValuesReference) JSONSchema() *jsonschema.Schema {
    39  	r := &jsonschema.Reflector{
    40  		DoNotReference:             true,
    41  		RequiredFromJSONSchemaTags: true,
    42  		KeyNamer:                   strcase.SnakeCase, // for action.ChartPathOptions
    43  	}
    44  
    45  	type values *ValuesReference
    46  	schema := r.Reflect(values(v))
    47  	schema.OneOf = []*jsonschema.Schema{
    48  		{
    49  			Type: "string",
    50  		},
    51  		{
    52  			Type: "object",
    53  		},
    54  	}
    55  	schema.Type = ""
    56  
    57  	return schema
    58  }
    59  
    60  // UnmarshalYAML flexible config.
    61  func (v *ValuesReference) UnmarshalYAML(node *yaml.Node) error {
    62  	type raw ValuesReference
    63  	var err error
    64  	switch node.Kind {
    65  	// single value or reference to another value
    66  	case yaml.ScalarNode, yaml.AliasNode:
    67  		err = node.Decode(&v.Src)
    68  	case yaml.MappingNode:
    69  		err = node.Decode((*raw)(v))
    70  	default:
    71  		err = ErrUnknownFormat
    72  	}
    73  
    74  	if err != nil {
    75  		return fmt.Errorf("failed to decode values reference %q from YAML: %w", node.Value, err)
    76  	}
    77  
    78  	return nil
    79  }
    80  
    81  // MarshalYAML is used to implement Marshaler interface of gopkg.in/yaml.v3.
    82  func (v *ValuesReference) MarshalYAML() (any, error) {
    83  	return struct {
    84  		Src string
    85  		Dst string
    86  	}{
    87  		Src: v.Src,
    88  		Dst: v.Dst,
    89  	}, nil
    90  }
    91  
    92  func (v *ValuesReference) isURL() bool {
    93  	return helper.IsURL(v.Src)
    94  }
    95  
    96  // Download downloads values by source URL and places to destination path.
    97  func (v *ValuesReference) Download(ctx context.Context) error {
    98  	if err := helper.Download(ctx, v.Dst, v.Src); err != nil {
    99  		return fmt.Errorf("failed to download values %s -> %s: %w", v.Src, v.Dst, err)
   100  	}
   101  
   102  	return nil
   103  }
   104  
   105  // Get returns destination path of values.
   106  // func (v *ValuesReference) Get() string {
   107  //	return v.Dst
   108  // }
   109  
   110  // SetUniq generates unique file path based on provided base directory, release uniqname and sha1 of source path.
   111  func (v *ValuesReference) SetUniq(dir string, name uniqname.UniqName) *ValuesReference {
   112  	h := crypto.MD5.New()
   113  	h.Write([]byte(v.Src))
   114  	hash := h.Sum(nil)
   115  	s := hex.EncodeToString(hash)
   116  
   117  	v.Dst = filepath.Join(dir, "values", name.String(), s+".yml")
   118  
   119  	return v
   120  }
   121  
   122  // ProhibitDst Dst now is public method.
   123  // Dst needs to marshal for export.
   124  // Also, dst needs to unmarshal for import from plan.
   125  func ProhibitDst(values []ValuesReference) error {
   126  	for i := range values {
   127  		v := values[i]
   128  		if v.Dst != "" {
   129  			return fmt.Errorf("dst %q not allowed here, this field reserved", v.Dst)
   130  		}
   131  	}
   132  
   133  	return nil
   134  }
   135  
   136  // SetViaRelease downloads and templates values file.
   137  // Returns ErrValuesNotExist if values can't be downloaded or doesn't exist in local FS.
   138  func (v *ValuesReference) SetViaRelease(
   139  	ctx context.Context,
   140  	rel Config,
   141  	dir, templater string,
   142  	renderedFiles *renderedValuesFiles,
   143  ) error {
   144  	if v.Renderer == "" {
   145  		v.Renderer = templater
   146  	}
   147  
   148  	v.SetUniq(dir, rel.Uniq())
   149  
   150  	l := rel.Logger().WithField("values src", v.Src).WithField("values Dst", v.Dst)
   151  
   152  	l.Trace("Building values reference")
   153  
   154  	data := struct {
   155  		Release Config
   156  	}{
   157  		Release: rel,
   158  	}
   159  
   160  	err := v.fetch(ctx, l)
   161  	if err != nil {
   162  		return err
   163  	}
   164  
   165  	opts := []template.TemplaterOptions{
   166  		template.SetDelimiters(v.DelimiterLeft, v.DelimiterRight),
   167  	}
   168  
   169  	if renderedFiles != nil {
   170  		buf := &strings.Builder{}
   171  		defer func() {
   172  			renderedFiles.Add(v.Src, buf)
   173  		}()
   174  
   175  		opts = append(opts,
   176  			template.CopyOutput(buf),
   177  			template.AddFunc("getValues", func(filename string) (any, error) {
   178  				s := renderedFiles.Get(filename).String()
   179  
   180  				var res any
   181  				err := yaml.Unmarshal([]byte(s), &res)
   182  
   183  				//nolint:wrapcheck
   184  				return res, err
   185  			},
   186  			))
   187  	}
   188  	if v.isURL() {
   189  		err = template.Tpl2yml(ctx, v.Dst, v.Dst, data, v.Renderer, opts...)
   190  	} else {
   191  		err = template.Tpl2yml(ctx, v.Src, v.Dst, data, v.Renderer, opts...)
   192  	}
   193  
   194  	if err != nil {
   195  		return fmt.Errorf("failed to render %q values: %w", v.Src, err)
   196  	}
   197  
   198  	return nil
   199  }
   200  
   201  func (v *ValuesReference) fetch(ctx context.Context, l *log.Entry) error {
   202  	if v.isURL() {
   203  		err := v.Download(ctx)
   204  		if err != nil {
   205  			l.WithError(err).Warnf("%q skipping: cant download", v.Src)
   206  
   207  			return ErrValuesNotExist
   208  		}
   209  	} else if !helper.IsExists(v.Src) {
   210  		l.Warn("skipping: local file not found")
   211  
   212  		return ErrValuesNotExist
   213  	}
   214  
   215  	return nil
   216  }
   217  
   218  func (rel *config) BuildValues(ctx context.Context, dir, templater string) error {
   219  	vals := rel.Values()
   220  
   221  	wg := parallel.NewWaitGroup()
   222  	wg.Add(len(vals))
   223  
   224  	// we need to keep rendered values in memory to use them in other values
   225  	renderedValuesMap := newRenderedValuesFiles()
   226  	// we need to keep track of values that we need to delete (e.g. non-existent files)
   227  	toDeleteMap := make(map[*ValuesReference]bool)
   228  
   229  	// just in case of dependency cycle or long http requests
   230  	ctx, cancel := context.WithTimeout(ctx, time.Minute)
   231  	defer cancel()
   232  
   233  	l := rel.Logger()
   234  
   235  	for i := range vals {
   236  		go func(v *ValuesReference) {
   237  			defer wg.Done()
   238  
   239  			l := l.WithField("values", v)
   240  
   241  			err := v.SetViaRelease(ctx, rel, dir, templater, renderedValuesMap)
   242  			switch {
   243  			case !v.Strict && errors.Is(ErrValuesNotExist, err):
   244  				l.WithError(err).Warn("skipping values...")
   245  				toDeleteMap[v] = true
   246  			case err != nil:
   247  				l.WithError(err).Error("failed to build values")
   248  
   249  				wg.ErrChan() <- err
   250  			}
   251  		}(&vals[i])
   252  	}
   253  
   254  	err := wg.WaitWithContext(ctx)
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	for i := len(vals) - 1; i >= 0; i-- {
   260  		if toDeleteMap[&vals[i]] {
   261  			vals = slices.Delete(vals, i, i+1)
   262  		}
   263  	}
   264  	rel.ValuesF = vals
   265  
   266  	return nil
   267  }
   268  
   269  type renderedValuesFiles struct {
   270  	bufs map[string]fmt.Stringer
   271  	cond *sync.Cond
   272  }
   273  
   274  func newRenderedValuesFiles() *renderedValuesFiles {
   275  	r := &renderedValuesFiles{
   276  		bufs: make(map[string]fmt.Stringer),
   277  	}
   278  	r.cond = sync.NewCond(&sync.Mutex{})
   279  
   280  	return r
   281  }
   282  
   283  func (r *renderedValuesFiles) Add(name string, buf fmt.Stringer) {
   284  	r.cond.L.Lock()
   285  	r.bufs[name] = buf
   286  	r.cond.Broadcast()
   287  	r.cond.L.Unlock()
   288  }
   289  
   290  func (r *renderedValuesFiles) Get(name string) fmt.Stringer {
   291  	r.cond.L.Lock()
   292  	for r.bufs[name] == nil {
   293  		r.cond.Wait()
   294  	}
   295  	r.cond.L.Unlock()
   296  
   297  	return r.bufs[name]
   298  }