Helm upgrade dynamically with Go

Sun, May 7, 2023 4-minute read

Helm upgrade dynamically with Go

I wrote a python file to create helm upgrade commands dynamically based on environment variables.

Not happy with that I also wrote it in Go. I much prefer it because its easier to distribute and has zero dependencies.

// Auto generate a helm deployment for use across multiple projects in CI.
// To use this the following is required:
//
//   - Environment variables set with the values needed. Read carefully for how to use each
//     type of variable.
//
//   - GitlabCI, this is primarily designed to be used in GitlabCI using its includes and
//     artifact generation functionality
//
//     See comments for how to use local environment variables to set values for helm.
//
// To use this in GitlabCI you only need to set the 'variables' block with your required values.
// Typically, this job will generate an artifact with the script as its output which a deployment
// job will have a needs relationship with and will then execute the script. YMMV
package main

import (
	"html/template"
	"log"
	"os"
	"strings"
)

// Environment variables, to use HELM_SET_FILE_PREFIX and HELM_SET_PREFIX
// the following considerations must be taken into a account.
// helm's '--set' and '--set-file' are used to override yaml fields inside a values.yml file.
// Using the following yaml as a example we'll illustrate how to use these envvars.
//
// example values.yml
// db
//
//	name: myDatabase
//	password: ""
//	caCert: {}
//
// Shell environment variables do not allow for "." notation. To get around this limitation
// it is expected that any "_" after the prefix will be translated into a "."
// For instance, HELM_SET_db_password=myPass would become '--set db.password=myPass'
// HELM_SET_FILE_db_caCert=/tmp/path/0/ca.crt becomes '--set-file db.caCert=/tmp/path/0/ca.crt'
//
// The outlier here is HELM_VALUES which is always provided as a string such as HELM_VALUES=helm/values.yml,helm/values2.yml
// which generates '--values helm/values.yml,helm/values2.yml'
const (
	HELM_SET_PREFIX      = "HELM_SET_VALUE_"
	HELM_SET_FILE_PREFIX = "HELM_SET_VALUE_"
	HELM_VALUES          = "HELM_VALUES"
)

type Values struct {
	Key   string
	Value string
}

func extractFileAndSetFile(re_prefix string) []Values {
	var values []Values
	for _, env := range os.Environ() {
		if strings.HasPrefix(env, re_prefix) {
			trimmedValues := strings.SplitN(env, re_prefix, 2)
			splitOnEquals := strings.SplitN(trimmedValues[1], "=", 2)
			dotValue := strings.ReplaceAll(splitOnEquals[0], "_", ".")
			value := Values{
				Key:   dotValue,
				Value: splitOnEquals[1],
			}
			values = append(values, value)
		}
	}
	return values
}

func extractValuesFiles(re_prefix string) []string {
	var values []string
	for _, env := range os.Environ() {
		if strings.HasPrefix(env, re_prefix) {
			kv := strings.SplitN(env, "=", 2)
			v := kv[1]
			values = append(values, v)
		}
	}
	return values
}

type TemplateData struct {
	Release     string
	ChartPath   string
	SetValue    []Values
	SetFile     []Values
	ValuesFiles []string
	// Allow for --dry-run to be passed in the script; usefule for debugging
	// or when wanting to use helm to template but kubectl to execute/apply
	DryRun any
}

func main() {
	setValues := extractFileAndSetFile(HELM_SET_PREFIX)
	setFiles := extractFileAndSetFile(HELM_SET_FILE_PREFIX)
	valuesFiles := extractValuesFiles(HELM_VALUES)
	release := os.Getenv("HELM_RELEASE")
	chartPath := os.Getenv("HELM_CHART_PATH")

	data := TemplateData{
		Release:     release,
		ChartPath:   chartPath,
		SetValue:    setValues,
		SetFile:     setFiles,
		ValuesFiles: valuesFiles,
		DryRun:      os.Getenv("DRYRUN"),
	}

	temp := template.Must(template.New("helm").Parse(helmTemplate))

	err := temp.Execute(os.Stdout, data)
	if err != nil {
		log.Fatal(err)
	}

}

var helmTemplate = `#!/bin/bash

helm upgrade --install {{ .Release }} {{ .ChartPath }} \
	{{ if .SetValue -}}
		{{- range .SetValue -}}
	--set '{{ .Key }}={{ .Value }}' \
		{{- end -}}
	{{- end }}
	{{ if .SetFile }}
		{{- range .SetFile -}}
	--set-file '{{ .Key }}={{ .Value }}' \
		{{- end -}}
	{{- end }}
	{{ if .ValuesFiles }}
		{{- range .ValuesFiles -}}
	--values '{{ . }}' \
		{{- end -}}
	{{- end }}
	--atomic --timeout 300s {{ if .DryRun }}--dry-run{{ end }}
`

To build this as a distributable binary is as simple as go build main.go. You can name the binary with go build -o <name> main.go. The run the binary and pipe its output to a script for execution when required. e.g. go run main.go > deploy-helm.sh.

It is a simple solution and would be keen to here how others solve it.

Tags:

#helm #go #templates