Feature Friday #28: Restricting individual promises using if and unless

Posted by Nick Anderson
September 20, 2024

Class expressions are powerful. They let you restrict the context for multiple promises in a single statement. What if you want to further control the context of a specific promise?

Let’s take a look at a contrived example:

/tmp/feature-friday-28-0.cf
bundle agent __main__
{
  reports:
    "I am running $(sys.os_release[PRETTY_NAME])";
    linux::
      "I love Linux!";
    linux.ubuntu::
      "Especially Ubuntu.";
    linux.redhat::
      "Especially RedHat.";
    linux.!(ubuntu|redhat)::
      "But not RedHat or Ubuntu.";
}
command
cf-agent -Kf /tmp/feature-friday-28-0.cf
output
R: I am running Ubuntu 22.04.4 LTS
R: I love Linux!
R: Especially Ubuntu.

Here, we have a report showing the distribution we’re running through class expressions protecting the individual promises. We would see I love Linux! on Linux hosts. Depending on the specific distribution running we would see Especially Ubuntu. or Especially RedHat.. If we are running something other than those two distributions But not RedHat or Ubuntu. would be reported.

We can make this policy a bit less repetitive by leveraging if and unless to restrict the individual promises that are all available when linux is defined:

/tmp/feature-friday-28-1.cf
bundle agent __main__
{
  reports:
    "I am running $(sys.os_release[PRETTY_NAME])";
    linux::
      "I love Linux!";
      "Especially Ubuntu." if => "ubuntu";
      "Especially RedHat." if => "redhat";
      "But not RedHat or Ubuntu." unless => or( "ubuntu",
                                                "redhat" );
}

By executing this policy, we see that it returns the exact same output:

command
cf-agent -Kf /tmp/feature-friday-28-1.cf
output
R: I am running Ubuntu 22.04.4 LTS
R: I love Linux!
R: Especially Ubuntu.

The if and unless attributes also let us use functions which can be useful for a cases where you only use the restriction once, without defining a class separately.

/tmp/feature-friday-28-2.cf
bundle agent __main__
{
  reports:
    "I am running $(sys.os_release[PRETTY_NAME])";
    linux::
      "I love Linux!";
      "Especially $(with)."
        with => "$(sys.os_release[NAME])",
        # Case insensitive `(?i)` match against classes contining ubuntu or redhat
        if => regcmp( "(?i).*(ubuntu|redhat).*",
                      "$(sys.os_release[PRETTY_NAME])" );
      "But not RedHat or Ubuntu." unless => or( "ubuntu",
                                                "redhat" );
}

Again, we can see executing this policy results in the same output.

command
cf-agent -Kf /tmp/feature-friday-28-2.cf
R: I am running Ubuntu 22.04.4 LTS
R: I love Linux!
R: Especially Ubuntu.

To a large extent, if and unless are opposites, and if => not() should be equivalent to unless.

Ignoring the not() function for a second, if and unless are exact opposites, in any case where if would skip a promise, unless would evaluate it, and vice versa. This holds true even for the more weird situations (edge cases), such as when the right-hand side is unresolved.

That’s not the case for the not() function. There are cases (unresolved variables and function calls) where if => x and if => not(x) behave the same when the right-hand side is unresolved. In both of these cases, the promise would be skipped.

bundle agent main
{
  classes:
      "a" if => "$(no_such_var)";      # Will be skipped
      "b" if => not("$(no_such_var)"); # Will be skipped
}

In other words, unresolved variables in function calls cause skipping. Similarly, if defaults to skipping, while unless defaults to evaluating. Strictly speaking, if and unless are true opposites, while if and if => not() are not.

So, if you are interested in what should be the default behavior when variables/function calls are not resolved. Then use if => not() when you want to default to skipping, and unless when you want to default to evaluating.

For more info, see the docs for if and unless.

Happy Friday! 🎉