Uploaded image for project: 'CFEngine Community'
  1. CFEngine Community
  2. CFE-3160

Behavior of if and unless when there are unexpanded variables

    XMLWordPrintable

    Details

    • Type: Task
    • Status: Done
    • Priority: Medium
    • Resolution: Fixed
    • Affects Version/s: None
    • Fix Version/s: 3.15.0
    • Component/s: None
    • Labels:
      None

      Description

      Changing the behavior of if/unless

      Introduction

      Follow-up from:

      https://tracker.mender.io/browse/CFE-2689

      For edge cases (unresolved variables) if and unless have unexpected behavior.

      Case in point:

      bundle agent main
      {
          classes:
            "a"
              expression => "any",
              if => "$(no_such_var)";
            "b"
              expression => "any",
              if => not("$(no_such_var)");
          reports:
            a::
              "a!!!";
            b::
              "b!!!";
      }
      
      $ test.cf -K
      $
      

      Approach A - Strictly logical

      • if => <x> should always be opposite to if => not(<x>) (even for edge cases).
      • if => <x> should always be opposite to unless => <x> (even for edge cases).
      • unless => <x> should always be equivalent to if => not(<x>) (even for edge cases).
      • if => <x> should always be equivalent to unless => not(<x>) (even for edge cases).
      • if => <x> should always be equivalent to if => not(not(<x>)) (even for edge cases).
      • And so on...

      Note that not() is not special, it's just a function call. The proper way to fix this problem, would be to evaluate all unexpanded strings to false / undefined on the last evaluation pass, and then evaluate all the possibly nested function calls to receive a consistent result.

      Additionally, we should consider what cases should emit an error and what is considered normal and allowed.

      Needless to say, this would be a big change to evaluation, not to be considered for a patch release, and has to be communicated clearly with users.

      Approach B - Useful skipping

      • Promises using if are only evaluated if the right side is evaluated to true
        • If the right side is never evaluated, or evaluates to false, the promise is skipped.
        • For cases where the right side is never evaluated, if and if => not() are both skipped (not opposite).
      • unless evaluates a promise, unless the right hand side evaluates to true
        • unless is a true opposite of if, it defaults to evaluate, so in all cases where if would skip, unless will evaluate.
        • if the right side is never evaluated, or evaluates to false, the promise is evaluated 
        • For cases where the right side is never evaluated, unless and unless => not() are both evaluated (not opposite).
        • unless is not equivalent to if => not(), since it defaults to evaluating the promise, rather than skipping. This means that unless has practical use outside of being a shorthand for if => not()

      (Another benefit of this approach is that it's very close to how it currently works, implementing it would require a lot less work, and likely not break any existing policy).

      Why "if" should not be opposite of "if not" in edge cases

      This example best illustrates a case where it is very useful that if and if => not() are not opposites:

      bundle agent main
      {
          classes:
              "key_missing" if => not(fileexists("$(filename)"));
              "key_exists" if => fileexists("$(filename)"));
      }
      

      If filename is missing, I want both promises to be skipped. I don't want to pretend to know that the key is missing, if I didn't truly evaluate the right hand side, with properly expanded variables.

      Comparison using examples

      Please note that these examples don't resemble real life use cases,
      they are provided to explain / specify how the 2 approaches work, in a rather technical way, in certain edge cases.
      In normal cases (marked as "obvious") the outcome and behavior of both approaches are the same / similar.

      Assuming literal_class is set,
      $(undefined_var) is an undefined variable (doesn't expand),
      and $(defined_var) expands to a class (or class expression) which is set / true.

      # Policy Approach A - Strict logic Approach B - Usefully skip Comments
      1 if => "literal_class" Evaluated Evaluated Obvious
      2 if => "$(defined_var)" Evaluated Evaluated Obvious
      3 if => "$(undefined_var)" Skipped Skipped Obvious
      4 if => not("literal_class") Skipped Skipped Obvious
      5 if => not("$(defined_var)") Skipped Skipped Obvious
      6 if => not("$(undefined_var)") Evaluated Skipped [6]
      7 if => strcmp("$(undefined_var)", "string") Skipped Skipped Obvious
      8 if => not(strcmp("$(undefined_var)", "string")) Evaluated Skipped [8]
      9 unless => "literal_class" Skipped Skipped Obvious
      10 unless => "$(defined_var)" Skipped Skipped Obvious
      11 unless => "$(undefined_var)" Evaluated Evaluated [11]
      12 unless => not("$(undefined_var)") Skipped Evaluated [12]
      13 unless => strcmp("$(undefined_var)", "string") Evaluated Evaluated [13]
      14 unless => not(strcmp("$(undefined_var)", "string")) Skipped Evaluated [14]

      Green outcomes are backwards compatible, red outcomes are breaking changes, yellow is new behavior (where it would previously error).

      [6]: A would evaluate, opposite of #3. B is how it works currently, skipped because of unexpanded variable.

      [8]: A would evaluate, opposite of #7. B is how it works currently, skipped because of unexpanded variable.

      [11]: Maybe obvious, but to spell it out: both would evaluate, because it should have opposite behavior to #3.

      [12]: A would skip, opposite to #11, and equivalent to #3. B would evaluate, opposite to #6.

      [13]: Although evaluation is different internally, here both cases should have opposite outcome to #7.

      [14]: A is skipped, opposite to #13. B is evaluated, opposite to #8.

      As a side note: Many of these examples don't make that much sense, from the perspective of Approach B.
      In approach B, when there are unresolved variables, it doesn't care about the function calls.
      not() is just a function call.
      Adding a not() or any other function call in these cases, doesn't make a difference.
      That is the main difference, in approach A, function calls are evaluated when variables are unresolved,
      meaning if => <x> is always opposite to if => not(<x>).
      In Approach B this is normally true, but not in edge cases (when there are unresolved variables).

        Attachments

          Issue Links

            Activity

              People

              • Assignee:
                olehermanse Ole Herman Schumacher Elgesem
                Reporter:
                olehermanse Ole Herman Schumacher Elgesem
              • Votes:
                0 Vote for this issue
                Watchers:
                4 Start watching this issue

                Dates

                • Created:
                  Updated:
                  Resolved:

                  Summary Panel