How can I conditionally set a variable in a Go template based on an expression which may cause an error if not wrapped with an if statement
Asked Answered
A

3

30

Question

How do I do something like this:

{{ $use_ssl := (ne $.Env.CERT_NAME "") }}

where $.Env.CERT_NAME may be nil/undefined. If it is nil, it gives this error:

at <ne $.Env.CERT_NAME "">: error calling ne: invalid type for comparison

Note: I have no control over the objects passed in to the Go template so have to solve this entirely within the template itself.

What I've tried

I tried to work around by first checking to see if it is non-empty:

{{ $use_ssl := ( ($.Env.CERT_NAME) && (ne $.Env.CERT_NAME "") ) }}

but it gives this error:

unexpected "&" in operand

So I switched to this, which is allowed syntactically:

{{ $use_ssl := (and ($.Env.CERT_NAME) (ne $.Env.CERT_NAME "") ) }}

but then I lose the short-circuit evaluation that I would get with the && operator and I'm back to getting this error again:

at <ne $.Env.CERT_NAME ...>: error calling ne: invalid type for comparison

Okay, I thought, if I can't do it in a nice one-liner, and Go doesn't have a ternary operator, that's fine, I'll just do it the idiomatic Go way, which is apparently if/else.

{{ if $.Env.CERT_NAME }}
  {{ $use_ssl := (ne $.Env.CERT_NAME "") }}
{{ else }}
  {{ $use_ssl := false }}
{{ end }}

But then of course I run into scoping issues, because if inexplicably (or at least annoyingly) creates a new variable scope (unlike in Ruby/ERB templates that I am more used to), so apparently I can't even do this:

{{ if $.Env.CERT_NAME }}
  {{ $use_ssl := true }}
{{ else }}
  {{ $use_ssl := false }}
{{ end }}
# $use_ssl: {{ $use_ssl }}

without getting this error now:

undefined variable "$use_ssl"

"No sweat", I thought. I'll just declare the variable in the outside scope so it will have the correct scope and the inner scope will still be able to change that variable (the one with the outer scope). (That's how it works in Ruby, by the way.)

Nope, apparently all that does is create 2 different variables with the same name but different scopes (how's that for confusing!):

{{ $use_ssl := false }}
{{ if $.Env.CERT_NAME }}
  {{ $use_ssl := true }}
  # $use_ssl: {{ $use_ssl }}
{{ else }}
  {{ $use_ssl := false }}
{{ end }}
# $use_ssl: {{ $use_ssl }}

  # $use_ssl: true
# $use_ssl: false

I've also tried inlining the if statement like this:

{{ $use_ssl := if $.Env.CERT_NAME { true } }}

but that gives this error:

unexpected <if> in command

Apparently if statements can't be used as expressions in Go like they can in Ruby?

I also get syntax errors if I try the various alternatives to the ternary operator suggested in What is the idiomatic Go equivalent of C's ternary operator?, like:

c := map[bool]int{true: 1, false: 0} [5 > 4]

And sure, you can read variables from outer scopes in inner scopes with $.something but how do you set $.something in the inner scope?

If not like that, then how??

So what have I missed? Is this even possible in a Go template?

Go templates (I'm new to them) seem very limiting. Almost everything you can do in Go itself is not possible in a template. But hopefully there is a workaround...

http://play.golang.org/p/SufZdsx-1v has a clever workaround involving creating a new object with {{$hasFemale := cell false}} and then setting a value with $hasFemale.Set true, but how can I do something like that without having access to the code that evaluates the template (where they are calling template.FuncMap)?

Others who have tried and failed

https://groups.google.com/forum/#!topic/golang-nuts/MUzNZ9dbHrg

This is the biggest (and only) problem I have with Go. I simply do not understand why this functionality has not been implemented yet.

When the template package is used in a generic context (eg. Executing an arbitrary template file with an arbitrary json file), you might not have the luxury of doing precalculations.

https://github.com/golang/go/issues/10608

This is probably as designed, but it would be really nice if there was a way to get the changed $v outside of the conditional.

This is the #1 template question we get over at Hugo (https://github.com/spf13/hugo). We have added a hack [$.Scratch.Set "v1" 123] to work around it for now ...

Atiptoe answered 24/3, 2016 at 20:43 Comment(1)
Thank you for making a great summary, I made a similar journey and now I finally understand why the {{ if ... }} assignment here {{ end }} failed!Innsbruck
I
27

Currently working solution

If you update the scope rather than declare a variable, either by using set or by using merge you will be able to access it outside of the if clause.

{{- if eq .podKind "core" -}}
{{- $dummy := set . "matchNodePurpose" .Values.scheduling.corePods.nodeAffinity.matchNodePurpose }}
{{- else if eq .podKind "user" -}}
{{- $dummy := set . "matchNodePurpose" .Values.scheduling.userPods.nodeAffinity.matchNodePurpose }}
{{- end -}}

# Will now output what you decided within an if/else if clause
{{- .matchNodePurpose }}

Future solution (Available with Helm 2.10)

I came across the same question in the context of Helm templates which are go templates with some additional features pre-installed such as Sprig.

28 March 2018, in this pr a Terenary operator was added to Sprig, and in time Helm will have them as well solving the issue both you and I have.

terenary

true | ternary "b" "c" will return "b"

false | ternary "b" "c" will return "c"

Innsbruck answered 2/4, 2018 at 3:32 Comment(4)
Interesting. Not that it's available in plain Go templates, but if it were, how would I use it to handle this case where $cert_name may be null? I assume a null input is treated like false? {{ $use_ssl := (ne $cert_name "") }} So maybe something like this to handle the null case? {{ $use_ssl := (ne ($cert_name | ternary $cert_name "") "") }}Atiptoe
If you would have the sprig functions, you could use their default function as well: masterminds.github.io/sprig/defaults.html (you can use it with pipes as well like $myPerhapsNullVar | default "defaultValue")Innsbruck
Helm users might also do something tricky with the tpl function: github.com/kubernetes/helm/pull/2350Innsbruck
I love this solution because I have a much more comlex if/else if/else so I don't want to use the ternary. Instead of the dummy-variable I use {{- set … | join | trunc 0 -}}Milkwort
T
16

Your fourth try would work if you'd use the assignment operator instead of the definition operator:

{{ $use_ssl := false }}
{{ if $.Env.CERT_NAME }}
  {{ $use_ssl = true }} # notice the missing colon!
{{ else }}
  {{ $use_ssl = false }}
{{ end }}
Tabbie answered 6/11, 2019 at 8:34 Comment(3)
Unfortunately, it's not working: Unable to parse template: unexpected "=" in operandGuesswarp
I believe this functionality was added in a later version of Go, hence why it probably didn't work for the OP in 2016 but would work now with modern version of Go.Vagabond
want to +1 as this is the answer in latest versions of Go. pkg.go.dev/text/template#hdr-VariablesOdyssey
A
8

I think I may have found a workaround for my particular problem, though I would love to hear other answers that solve the problem more generally.

Came across this line in https://github.com/jwilder/nginx-proxy/blob/1e0b930/nginx.tmpl#L91:

{{ $default_host := or ($.Env.DEFAULT_HOST) "" }}

which gave me another idea to try. I guess the solution is to use a combination of or and set extra temporary variables...

{{ $cert_name := or $.Env.CERT_NAME "" }}
{{ $use_ssl := ne $cert_name "" }}
Atiptoe answered 24/3, 2016 at 21:16 Comment(1)
Official documentation for the default value usage: github.com/hashicorp/consul-template#envPedlar

© 2022 - 2024 — McMap. All rights reserved.