Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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 ...

like image 307
Tyler Rick Avatar asked Mar 24 '16 20:03

Tyler Rick


2 Answers

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"

like image 157
consideRatio Avatar answered Oct 16 '22 12:10

consideRatio


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 "" }} 
like image 32
Tyler Rick Avatar answered Oct 16 '22 13:10

Tyler Rick