Hi! My name is Alex, and I created helmtk. I'd like to share a bit about how it came to be.
For the past couple years, I helped maintain a public Helm chart for a popular product. This was my first experience deploying with Helm and maintaining a Helm chart. I quickly learned that there are some issues that both Helm chart developers and users frequently encounter:
yes/no as a boolean (e.g. the norway bug)Overall, it was a fairly frustrating and error prone experience for me, and unfortunately also for our users. Engineers and technical sales teams would often lament about the prevalence of YAML in the devops world. It's too easy to break things with a single stray character or a subtly incorrect list of objects, or an unquoted database password.
Over time I learned some tricks of the trade: careful use of {{- and
nindent,
watch out for yes, use | quote and | toYaml,
give up on the idea of readable control flow in
templates,
pass arguments to helpers using (list arg arg) or (dict "arg" arg) and you
might need
to pass the root $ object too. Helm doesn't come with a builtin approach for testing,
so we put something together with Go that was somewhat helpful.
There's a lot to learn. And unfortunately, none of is what made our product special. It's a lot of work for what is essentially just the installer and configuration. Honestly, after a couple years of experience, it still feels like trial and error. I'm never really confident about Helm template code I'm working on.
Here's an example of subtle behavior in the example HTTPRoute template that helm create creates (code is here). Can you spot it?
rules:
{{- range .Values.httpRoute.rules }}
{{- with .matches }}
- matches:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .filters }}
filters:
{{- toYaml . | nindent 8 }}
{{- end }}
backendRefs:
- name: {{ $fullName }}
port: {{ $svcPort }}
weight: 1
{{- end }}
If you don't provide matches, then the YAML - matches line is never emitted, and you get an invalid YAML resource because
rules is not an array. Maybe it's an unimportant edge case because every will define matches, but it's subtle nonetheless.
I also learned it's hard to get away from Helm. Early on, I proposed that we shouldn't support Helm, and instead we could write a custom generator — perhaps something that would construct Kubernetes resources in Go code and write out the final YAML. We'd have a type system and IDE support and a test suite and more. But, of course, that doesn't really work for customers and users. Helm is very popular. Teams have extensive systems built around Helm. Helm isn't just a templating tool, it's an ecosystem.
By the way, none of this is a knock on the creators of Helm. They solved real problems, quickly, and
created something
incredibly popular and useful. I think we now understand the problems of combining Go's
text/template language
with YAML, but it's easy to see these things in hindsight. I don't know the Helm devs, but I imagine
they too feel stuck.
How can we move this mountain, this vibrant ecosystem, away from text/template and YAML?
So, after being laid off recently and having some free time on my hands, I decided to try to scratch this itch.
The initial idea was this: could I compile a more structured language into a Helm chart? That way, people could work with a nice language, and still publish a standard Helm chart, so they wouldn't need to disrupt any existing ecosystem (users, developers, CI/CD, etc). Coffeescript, Typescript, and others did this with Javascript. Could it work with Helm templates?
I actually started out with HCL in mind, but I'm not
deeply familiar with HCL,
and I wasn't sure it would compile cleanly to Go's text/template syntax. Also, I've had
enough experience
writing parsers and compilers to feel comfortable thinking about a new language, and with Claude writing
the code
I had a working prototype in a few hours.
Brief tangent: There's a lot of interest and chatter about how useful AI tools like Claude are for development, so here's my data point. Claude was tremendously helpful in getting a prototype created — something I could tinker with and get a feel for.
It also got a lot of things working and made a bit of a mess, so I did have to go back through and clean a lot of things up. In fact, I still have a TODO list item to comb through the code, the parser design, etc to make sure the language is sound.
Here's a look at helmtk code (actually called htkl, which is an open source library)
# demo/helmtk/templates/example.helmtk
{
"ports": [
{ "containerPort": 8000, "name": "http" }
# if debug is enabled, include the JVM debug port
if Values.debug do
{ "containerPort": 5005, "name": "debug" }
end
# include any user-defined ports
for name, port in Values.tcpPorts do
{ containerPort: port, name: name }
end
]
}
Some things to note:
if and for)do/end (curly braces are used for objects). context — context variables are always named#
The helmtk eval command can be used to evaluate that. For example:
$ helmtk eval demo/ --set debug=true
example:
- ports:
- containerPort: 8000
name: http
- containerPort: 5005
name: debug
The helmtk compile command can be used to compile that to a Helm template:
$ helmtk compile demo
---
{
"ports": [
{
"containerPort": 8000,
"name": "http",
},
{{- if $.Values.debug -}}
{
"containerPort": 5005,
"name": "debug",
},
{{- end -}}
{{- range $name, $port := $.Values.tcpPorts -}}
{
"containerPort": {{ $port | toJson }},
"name": {{ $name | toJson }},
},
{{- end }}
],
}
Cool! Ok, so, now what? Now I have to convert a real Helm chart to helmtk. Oops, ya, that's actually really tedious.
It would be really nice if existing Helm charts could just be automatically imported into helmtk.
Originally, I thought I could do this by parsing the text/template syntax and writing
some heuristics to convert that into helmtk. Maybe that's possible, but I'm not smart enough to know.
The main problem is that it's really hard to infer the correct indentation from Helm template text.
After that, there are all sorts of Helm template patterns to recognize and translate. So that approach
didn't last long.
Of course, LLMs are very good at translating things, so that's how the decompiler currently works.
The helmtk decompile will read an existing Helm chart and use an LLM to translate it to
helmtk.
But, how do I make sure the translated output is correct?
I mentioned before that we hacked together a test suite for that popular, public Helm chart I maintained for the last couple years. I haven't looked at a lot of Helm charts, but I get the feeling that most don't have a testing tool.
I needed to ensure the output of the decompiler was correct, so I added a dead simple test framework to
helmtk.
Tests are written in javascript and run with the helmtk test.
$ cat demo/helmtk/tests/example.js
test("test debug port", t => {
t.Values.debug = true
let res = t.render()[0]
t.eq(res.ports[1].name, "debug")
})
$ helmtk test demo
example
✓ test debug port
1 passed, 0 failed, 1 total
Tests: 1 passed, 0 failed, 1 total
Test Suites: 1 total
Even without the helmtk language, I think this is a super useful tool. An LLM tool can easily generate an extensive test suite for a Helm chart. The tests are simple to read and write, and fast to run. Of course, there are other options like helm-unittest.
So now, the decompiler will first generate a test suite for the existing Helm chart. It uses
helmtk test --renderer helm
to verify the test suite against the existing Helm chart. Once that works, it generates helmtk code, and
runs the test suite
against the helmtk code. Finally, we can test against the compiled code using
helmtk test --renderer compiled
to verify that the compiled code also works (at the time of this writing, the decompiler doesn't include
this final step yet).
There's a fuzzer in the decompiler as well. It generates random values, evaluates the original Helm chart, evaluates the helmtk chart, and compares the output. This adds extra confidence that the helmtk chart is going to work as needed.
For some reason, I never had good IDE support set up for the Helm charts I worked on. I wanted to make sure that's available for helmtk.
I created a VS Code extension for helmtk, which supports syntax highlighting, code navigation (e.g. jump to template definition), running tests, etc.
I've been working on this for a few weeks now. It works...mostly. It's rough around the edges. helmtk is in a decent "alpha" or "beta" state. It's time to step back, get out of my bubble, and get in touch with some real humans.
I would love feedback on this project. I need people to download it, use it, tinker with it, and tell me why it works or doesn't work. Even if you think you don't want to use it, I'd like to better understand why this idea does or doesn't work for people.
Please reach out at feedback@helmtk.dev, join me on Slack,
file an issue on Github, whatever is easiest.
The decompiler has been the biggest project. It has been very tricky to teach
an LLM a new language, and how to translate it consistently and effectively.
The decompiler can translate most of the builtin Helm example chart (created with
helm create),
with only some minor manual tweaks needed. Sometimes it gets stuck.
On the other hand, while writing this, I ran it on my simple demo chart used above, and it failed miserably. So there's work to be done. But, the decompiler would be a one-time use, so as long as it can do 80-90% of the work, and not make a giant mess, I think it would be helpful.
If you made it this far, thank you! I appreciate it.
So, helmtk is a toolkit for Helm chart maintainers. It provides a structured language, which I hope helps simplify Helm chart development and avoids some pitfalls. It provides a simple test framework, and a decompiler which I hope makes importing existing charts easy.
helmtk is young and probably needs plenty more work. I hope to get some feedback on it.
Please reach out at feedback@helmtk.dev,
join me on Slack,
file an issue on Github, whatever is easiest.
Check out the install page for download links. helmtk is free for personal, non-commercial use, but feel free to trial it at work too. If you find it useful and want to continue using it, it's $100/year (total, not per seat).
The decompiler works with a custom LLM-based API that charges by tokens used. The payment system is not published yet, so if you want to try it out, reach out and I can get you a license key for testing.
Thanks again!