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 }