Skip to content

feat: accept strings that evaluates to bool in release condition#840

Closed
PatrykKlimowicz wants to merge 1 commit into
helmfile:mainfrom
PatrykKlimowicz:pklimowicz/accept-true-like-strings-in-release-condition
Closed

feat: accept strings that evaluates to bool in release condition#840
PatrykKlimowicz wants to merge 1 commit into
helmfile:mainfrom
PatrykKlimowicz:pklimowicz/accept-true-like-strings-in-release-condition

Conversation

@PatrykKlimowicz

Copy link
Copy Markdown

This adds more flexibility to helmfile conditioning by:

  • Adding: strconv.ParseBool() allows evaluating strings to booleans which gives possibility to extend the conditioning functionality
  • Changing: the error message displayed on the terminal when value for the condition field is neither in 'foo.enabled' format nor a string that can be evaluated to bool value
  • Removing: -

Resolves: #837

With this PR the following configuration:

environments:
  # by default there are no deployments enabled
  default:
    values:
      - foo:
           enabled: false
      - boo:
           enabled: false
      - bar:
           enabled: false
  # one env to rule them all
  all:
    values:
      - foo:
           enabled: true
      - boo:
           enabled: true
      - bar:
           enabled: true
  # One release at time starts here
  foo:
    values:
      - foo:
           enabled: true
      - boo:
           enabled: false
      - bar:
           enabled: false
  boo:
    values:
      - foo:
           enabled: false
      - boo:
           enabled: true
      - bar:
           enabled: false
  bar:
    values:
      - foo:
           enabled: false
      - boo:
           enabled: false
      - bar:
           enabled: true

can be minimized to:

environments:
  # one env to rule them all
  all:
    values:
      - foo:
           enabled: true
      - boo:
           enabled: true
      - bar:
           enabled: true
  # One release at time starts here
  foo:
    values:
      - foo:
           enabled: true
  boo:
    values:
      - boo:
           enabled: true
  bar:
    values:
      - bar:
           enabled: true

and then in the helmfile.yaml this can be specified:

releases:
  - name: foo
    chart: ./foo
    condition: '{{ .Values | get "foo.enabled" "false" }}'
  - name: boo
    chart: ./boo
    condition: '{{ .Values | get "boo.enabled" "false" }}'
  - name: bar
    chart: ./bar
    condition: '{{ .Values | get "foo.enabled" "false" }}'

and the following commands will work just fine:

# apply foo only
helmfile -e foo apply

# apply boo only
helmfile -e boo apply

# apply bar only
helmfile -e bar apply

# apply everything 
helmfile -e all apply

What are cons here?

Users will have to chose the way of defining the conditions and stick to it. The 'string to bool' (the one proposed in this PR) and 'variable path to bool' (the one that is currently in use) approaches cannot be mixed. Mixing of these two approaches will result in panic:

panic: environment values does not contain field 'sampleFieldName'

Hope you will find this useful and accept this PR! 😄

P.S. I did not add the docs yet, as I do not know if you will accept this PR. If there will be a green light, then I'm more than happy to update the documentation.

@yxxhero

yxxhero commented Apr 28, 2023

Copy link
Copy Markdown
Member

@PatrykKlimowicz Please fix DCO issue.

@PatrykKlimowicz PatrykKlimowicz force-pushed the pklimowicz/accept-true-like-strings-in-release-condition branch 2 times, most recently from 3b60198 to cf74c5d Compare April 28, 2023 22:07
@yxxhero

yxxhero commented Apr 28, 2023

Copy link
Copy Markdown
Member

Why not use installed field?

@mumoshu

mumoshu commented Apr 29, 2023

Copy link
Copy Markdown
Contributor

Hey @PatrykKlimowicz! Thanks for your contribution.
I've just started reviewing this, and I'd appreciate it if you could confirm one thing;

can be minimized to:

Every non-all environment seem to inherit missing values from the all environment. How does it work...? I thought that's not how helmfile is supposed to work. Without that, new the syntax like condition: '{{ .Values | get "foo.enabled" "false" }}' would be equivalent to today's condition: foo.enabled?

Sorry if I lost memory or missed something. Thanks in advance for your help!

@PatrykKlimowicz

Copy link
Copy Markdown
Author

Why not use installed field?

Correct me if I'm wrong, but releases marked with installed: false will be uninstalled in the next sync. With this proposal this won't be the case.

@PatrykKlimowicz

PatrykKlimowicz commented Apr 29, 2023

Copy link
Copy Markdown
Author

Hey @PatrykKlimowicz! Thanks for your contribution. I've just started reviewing this, and I'd appreciate it if you could confirm one thing;

can be minimized to:

Every non-all environment seem to inherit missing values from the all environment. How does it work...? I thought that's not how helmfile is supposed to work. Without that, new the syntax like condition: '{{ .Values | get "foo.enabled" "false" }}' would be equivalent to today's condition: foo.enabled?

Sorry if I lost memory or missed something. Thanks in advance for your help!

Sorry, I do not have the possiblity to provide the full description now. However, there is no inheritance between envs, they are still independent. I will update later today/tomorrow morning with the examples.

@PatrykKlimowicz

Copy link
Copy Markdown
Author

Hey @PatrykKlimowicz! Thanks for your contribution. I've just started reviewing this, and I'd appreciate it if you could confirm one thing;

can be minimized to:

Every non-all environment seem to inherit missing values from the all environment. How does it work...? I thought that's not how helmfile is supposed to work. Without that, new the syntax like condition: '{{ .Values | get "foo.enabled" "false" }}' would be equivalent to today's condition: foo.enabled?

Sorry if I lost memory or missed something. Thanks in advance for your help!

Ok, found some time to sit in front of my laptop. Right now there is a need to specify the envs as following:

environments:
  foo:
    values:
      - foo:
           enabled: true
      - boo:
           enabled: false
      - bar:
           enabled: false
  boo:
    values:
      - foo:
           enabled: false
      - boo:
           enabled: true
      - bar:
           enabled: false
  bar:
    values:
      - foo:
           enabled: false
      - boo:
           enabled: false
      - bar:
           enabled: true

and this is because of how condition has to be specified. Helmfile is forcing the approach of foo.enabled, hence we have to repeat the values to make the following works:

releases:
  - name: foo
    chart: ./foo
    condition: 'foo.enabled'
    values:
      - ./foo/values-extra.yaml

  - name: boo
    chart: ./boo
    condition: 'boo.enabled'
    values:
      - ./boo/values-extra.yaml

  - name: bar
    chart: ./bar
    condition: 'bar.enabled'
    values:
      - ./bar/values-extra.yaml

so if we ran the helmfile -e foo diff the conditions for boo and bar releases will be evaluated, it will turn out that value is false, so only foo will be enabled. But all used values in condition field must be defined for given environment.

This PR gives us possibility to minimize the code duplication in the environments section thanks to the use of get template function that provides us opportunity to specify the fallback value. We can do the following:

environments:
  foo:
    values:
      - foo:
           enabled: true
  boo:
    values:
      - boo:
           enabled: true
  bar:
    values
      - bar:
           enabled: true

However, to avoid panic in the ConditionEnabled function we have to change the way of defining the value in condition field. What we are looking for is the actual value of this foo.enabled var passed in example above. To be precise, string that can be converted to boolean. Now, the releases will look as following:

---
releases:
  - name: foo
    chart: ./foo
    condition: '{{ .Values | get "foo.enabled" "false" }}' # obtain value or fallback
    values:
      - ./foo/values-extra.yaml

  - name: boo
    chart: ./boo
    condition: '{{ .Values | get "boo.enabled" "false" }}' # obtain value or fallback
    values:
      - ./boo/values-extra.yaml

  - name: bar
    chart: ./bar
    condition: '{{ .Values | get "bar.enabled" "false" }}' # obtain value or fallback
    values:
      - ./bar/values-extra.yaml

After we ran the helmfile -e foo apply command, get function will try to obtain the values for foo.enabled, boo.enabled, bar.enabled vars, but we know that only foo.enabled is defined, hence for other two the fallback value false will be used as the entry for condition field. Since it is a string that we can convert to bool, both boo and bar releases won't be applied.

In the current approach of helmfile, we have to specify the default values in the each environment. This PR does not impact the helmfiles that people has created already. It also does not change the default helmfile behavior. It adds possibility to use the "special" strings as the value for condition field and thanks to the usage of get template function we can specify defaults in inline manner.

Why this is important to have this possibility?
Current helmfile # releases / # loc in whole environments section:

  • 2/12 - 6 lines per release
  • 3/24 - 8 lines per release
  • 4/40 - 10 lines per release
  • 5/60 - 12 lines per release
  • 6/84 - 14 lines per release

With this PR we can achieve # releases / # loc in whole environments section:

  • 2/8 - 4 lines per release
  • 3/12 - 4 lines per release
  • 4/16 - 4 lines per release
  • 5/20 - 4 lines per release

Happy to answer some more questions 😄

@PatrykKlimowicz

Copy link
Copy Markdown
Author

To give you some insights how exactly the code change is working:

  1. If condition is not defined or is "" return true, nil immediately
  2. If condition is string that is convertible to boolean, return it's bool representation
  3. Check if passed string which is referencing variable follows the convention of 'foo.enabled'
  4. Process given var, and return true/false

@mumoshu

mumoshu commented Apr 29, 2023

Copy link
Copy Markdown
Contributor

@PatrykKlimowicz Thanks for clarifying! Ah gotcha! So the key idea in this pull request is that you define default values in get template function calls in, whereas "ideally," the default and override values are defined under environments... correct?

Presuming that's correct, how about implementing some kind of "inheritance" to environments instead, like we've done for templates/releases in #606?

It could be implemented to look like this:

environments:
    # one env to rule them all
  all:
    values:
      - foo:
           enabled: true
      - boo:
           enabled: true
      - bar:
           enabled: true
  # One release at time starts here
  foo:
    inherit:
      environment: all
    values:
      - foo:
           enabled: true
  boo:
    inherit:
      environment: all
    values:
      - boo:
           enabled: true
  bar:
    inherit:
      environment: all
    values:
      - bar:
           enabled: true

or to avoid mixing "template" environments (like "all", which is not supposed to be used directly via -e all) and regular environments-

environmentTemplates:
    # one env to rule them all
  all:
    values:
      - foo:
           enabled: true
      - boo:
           enabled: true
      - bar:
           enabled: true

environments:
  # One release at time starts here
  foo:
    inherit:
      template: all
    values:
      - foo:
           enabled: true
  boo:
    inherit:
      template: all
    values:
      - boo:
           enabled: true
  bar:
    inherit:
      template: all
    values:
      - bar:
           enabled: true

or more simply, introduce some defaults like helmDefaults for `releases[]-

environmentDefaults:
  # one env to rule them all
  values:
  - foo:
       enabled: true
  - boo:
       enabled: true
  - bar:
       enabled: true

environments:
  # One release at time starts here
  foo:
    values:
      - foo:
           enabled: true
  boo:
    values:
      - boo:
           enabled: true
  bar:
    values:
      - bar:
           enabled: true

@PatrykKlimowicz

PatrykKlimowicz commented Apr 30, 2023

Copy link
Copy Markdown
Author

@mumoshu The key idea is to make the hardcoded nature of condition field more flexible. As a result this is possible:

define default values in get template function calls

The proposals you've made are kind of interesting! I'd say environmentDefaults is more elegant way of what I made with get template function.

I really like the clean nature of inherit + environmentsTemplate approach. Since it has been already implemented in the other places I believe it's good idea to "extend" this keyword on the environments section as well. I can work on this proposal - no problem 😄

Anyway, the idea from this PR can be independent from the inherit approach. Is there any strong reason that condition can only be foo.enabled and not true or false explicitly? And why only two levels of nesting are valid? Why we cannot use variable like foo.release.enabled? The hardcoded nature of this field is really strange. As you can see, the possiblity to pass to condition strings that can be evaluated to bool is pretty powerful and is achievable with small amount of changes in the code.

To sum up, it would be really nice to have this inherit functionality with environments section (I can create PR for that) AND it would be nice to make the condition field more flexible. WDYT?

This adds more flexibility to helmfile conditioning by:

- Adding: strconv.ParseBool() allows evaluating strings to booleans
          which gives possibility to extend the conditioning
          functionality
- Changing: the error message displayed on the terminal when value
            for the condition field is neither in 'foo.enabled' format
            nor a string that can be evaluated to bool value
- Removing: -

Resolves: helmfile#837
Signed-off-by: Patryk Klimowicz <[email protected]>
@yxxhero yxxhero force-pushed the pklimowicz/accept-true-like-strings-in-release-condition branch from cf74c5d to c77736f Compare May 8, 2023 05:25
@PatrykKlimowicz

Copy link
Copy Markdown
Author

@mumoshu any decisions? 😄

@mumoshu

mumoshu commented May 18, 2023

Copy link
Copy Markdown
Contributor

Is there any strong reason that condition can only be foo.enabled and not true or false explicitly?

Yes! That is to make the condition field look very similar to helm's Chart.yaml condition field, with takes a path in values.yaml.

@PatrykKlimowicz

Copy link
Copy Markdown
Author

Is there any strong reason that condition can only be foo.enabled and not true or false explicitly?

Yes! That is to make the condition field look very similar to helm's Chart.yaml condition field, with takes a path in values.yaml.

Ok, I'll propose this inherit + environmentsTemplate functionality in the new PR then 😄

@stale

stale Bot commented Jun 8, 2023

Copy link
Copy Markdown

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale Bot added the wontfix This will not be worked on label Jun 8, 2023
@yxxhero yxxhero added in progress and removed wontfix This will not be worked on labels Jun 8, 2023
@yxxhero

yxxhero commented Jun 8, 2023

Copy link
Copy Markdown
Member

@PatrykKlimowicz any updates?

@jouve

jouve commented Jun 15, 2023

Copy link
Copy Markdown
Contributor

you could define "default" values to false and only the overrides in environments:

values:
      - foo:
           enabled: false
        boo:
           enabled: false
        bar:
           enabled: false

environments:
  all:
    values:
      - foo:
           enabled: true
        boo:
           enabled: true
        bar:
           enabled: true
  foo:
    values:
      - foo:
           enabled: true
  boo:
    values:
      - boo:
           enabled: true
  bar:
    values:
      - bar:
           enabled: true

@PatrykKlimowicz

Copy link
Copy Markdown
Author

@PatrykKlimowicz any updates?

Sorry, got stuck with other work. I'll open PR tomorrow

@JuryA

JuryA commented Jul 12, 2023

Copy link
Copy Markdown

I suggest to extend it not only for bools, but also for other types - see my topic in discussions: #926

@mumoshu

mumoshu commented Jul 24, 2023

Copy link
Copy Markdown
Contributor

@JuryA Hey! Please also see this one #913 (comment) In short, I'm doubting that we are about to outgrow yaml + go template based solution....

Comment thread pkg/state/state.go
conditionSplit := strings.Split(r.Condition, ".")
if len(conditionSplit) != 2 {
return false, fmt.Errorf("Condition value must be in the form 'foo.enabled' where 'foo' can be modified as necessary")
return false, fmt.Errorf("condition value must be either in the form 'foo.enabled' where 'foo' can be modified as necessary or string that can be evaluated to bool")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I contend that introducing unnecessary complexity serves no purpose. The issue at hand aptly demonstrates this point. My reasoning is influenced by Helm's elegant handling of its dependencies[].condition parameter in the Chart.yaml file. This parameter singularly points to another key in $.Values, without any concern for the data type—be it boolean, string, or map. To elaborate, I often employ a key named enabled, which can take on a variety of values—true, "true", or more specialized values like "manual" or "auto". Such values are evaluated as true for dependency conditions for the simple reason that they are not null. Any deviation from this approach would be excessive. The condition is met when the targeted key is present and its value is not empty; otherwise, the condition remains unfulfilled. The choice of how to manage this rests solely with the Helm chart developer.

In the same vein, I propose the following criteria for this case, which negates the need for type-specific considerations:

  • Existence of Key
    • Contains a non-empty value? ➜ Condition: True
    • Lacks value? ➜ Condition: False
  • Absence of KeyCondition: False

@JuryA

JuryA commented Sep 13, 2023

Copy link
Copy Markdown

@JuryA Hey! Please also see this one #913 (comment) In short, I'm doubting that we are about to outgrow yaml + go template based solution...

Greetings. Apologies for the delayed response. I've taken some time to evaluate the benefits of both Jsonnet and CUE. Both have their unique advantages and optimally, supporting both would be beneficial. However, I understand this might complicate the codebase.

The immediate issue to address is the unavailability of some key fundamental information. Currently, obtaining this data requires cumbersome, less-than-ideal methods. This problem is magnified when considering cross-platform compatibility, affecting Windows, Linux, and macOS. Fortunately, one can rely on Git for this purpose. For example, on Windows, we can use:

git -c 'alias.cmd=!<some command> <its_args>' cmd

It's a somewhat inelegant and hacky solution, isn't it? 😬 But it ensures that the command will execute in Git Bash on Windows and in a suitable shell on other POSIX platforms.

It would be advantageous to directly provide some well-known information at runtime by Helmfile itself among other objects.

For example:

Helmfile Root Directory:

values:
  - Helmfile:
      Paths:
        helmfileRoot: !!str {{ exec "git" (list "-c" "alias.pwd=!pwd" pwd) | trim | osClean }}

PWD:

{{ $PWD := env "PWD" | osClean }}

Pertinent Paths:

{{ $Helmfile := .Values | get "Helmfile" (dict) }}
{{ $Self := $Helmfile | get "Self" (dict) }}
{{ $helmfilePath := $Self | getOrNil "helmfilePath" }}
{{ $helmfileDir := $helmfilePath | osDir }}
{{ if not $helmfilePath }}
  {{ print "\n\n.Values.Helmfile.Self.helmfilePath not defined!\n\n" | fail }}
{{ end }}

Other Paths:

  - Helmfile:
      Self:
        helmfileDir: &helmfileDir {{ $helmfileDir | osClean | quote }}
        helmfilePath: &helmfilePath {{ $helmfilePath | osClean | quote }}
        helmfileName: {{ $helmfilePath | osBase | quote }}
      Paths:
        helmfileDir: *helmfileDir
        helmfilePath: *helmfilePath

... and many others...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants