Change in behavior: Creating files by default

Posted by Lars Erik Wik
April 22, 2022

In the upcoming CFEngine 3.20 release we are making a change in the behaviour of the create attribute for the files promises that manage the entire content of a file. This includes promises with the template methods mustache, inline_mustache and cfengine; as well as promises with the content attribute.

The motivation behind these new changes is two-fold; make it easier to learn CFEngine policy language and understand what policy is doing, and to prevent CFEngine from creating empty configuration files.

Problem 1 - Readability and ease of use

Note: The examples below are for the content attribute for simplicity, but the same exact arguments apply to templates as well.

Consider the following policy:

bundle agent main
{
  files:
    "/tmp/hello.txt"
      content => "Hello, world!";
}

This might be the first piece of policy someone reads. It seems quite clear what the intent is here, the user wants a file called /tmp/hello.txt, with the content Hello, world!. However, historically, and before the content attribute was introduced, the behavior for files promises in CFEngine was to not create files by default. This can make sense in some scenarios, if you are only interested in changing very specific parts of a file (remove a line, change a permission, etc.).

The old-school way of specifying you want the file to be created if it doesn’t exist is using the create attribute:

bundle agent main
{
  files:
    "/tmp/hello.txt"
      content => "Hello, world!",
      create => "true";
}

Nobody knows instinctively that files promises work this way, that you have to specify create => "true", this is something you just have to learn (maybe try and fail first). On the other hand, our first example without create made a lot of sense, it seemed clear, and should just work. If we only want to do something if the file exists, we can express that perfectly in policy language:

bundle agent main
{
  files:
    "/tmp/hello.txt"
      content => "Hello, world!",
      if => fileexists("/tmp/hello.txt"); # Or $(this.promiser)
}

Now, this policy reflects how it was described in an English sentence, and it can even be understood by someone who doesn’t know CFEngine policy language. So, to make things easier to learn, read, and understand we are changing the default here so that files are created by default in the cases where it definitely makes sense; templates and promises which manage the full contents of the file. Making this change across all files promises, even those that just change a permission or a single line, would be a big change, and not something we want to rush.

Problem 2 - Empty files

Another related problem we’d like to fix is promises which should create a file and fill it with data from a template. Say you have some policy for hosting an Apache 2 web server. You may have a bundle including promises to make sure that the apache2 package is installed:

bundle agent webserver
{
  meta:
    webserver::
      "tags"
        slist => { "autorun" },
        comment => "Enable autorun";

  packages:
      "apache2"
        policy => "present",
        package_module => apt_get,
        comment => "Install apache2 package";

Furthermore you may have a promise to render the contents of the Apache 2 configuration file. In this example I’ve used the template method cfengine.

  vars:
      "name"
        string => "www.website.com",
        comment => "Website hostname";
      "admin"
        string => "admin@localhost",
        comment => "Website administrator";
      "root"
        string => "/var/www/website",
        comment => "Website root directory";

  files:
      "/etc/apache2/apache2.conf"
        create => "true",
        template_method => "cfengine",
        edit_template => "$(sys.workdir)/templates/apache2.conf.tmpl",
        classes => results("bundle", "config"),
        comment => "Edit apache2 config";

Finally, whenever changes are made to the Apache 2 config file, we’ll need to restart the Apache 2 service in order to bring the changes into effect.

  services:
    config_repaired::
      "apache2"
        service_policy => "restart",
        comment => "Restart apache2 service";
}

If you now bootstrap a fresh host to a hub with the following policy; the files promises would be executed first - due to CFEngine normal ordering - creating the configuration file with the rendered content. Next the packages promises would be executed, installing the “apache2” package. The installation script would see that the configuration file was already there, thus it would avoid overwriting it.

Perfect! Everything works as expected. But what if the rendering of the configuration file would fail to render? In this case, an empty configuration file would be created, causing the web server to break. This is not really what we want, is it? Of course you could work around this by controlling the policy execution; using the depends_on attribute:

  files:
      "/etc/apache2/apache2.conf"
        create => "true",
        template_method => "cfengine",
        edit_template => "$(sys.workdir)/templates/apache2.conf.tmpl",
        classes => results("bundle", "config"),
        comment => "Edit apache2 config",
        depends_on => { "apache2_package_present" };

  packages:
      "apache2"
        policy => "present",
        handle => "apache2_package_present",
        package_module => apt_get,
        comment => "Install apache2 package";

But still, it does not make sense that the files promise leaves behind an empty file. We’d like CFEngine to render the template and only create the file if there is content to put in it.

Conclusion

In its essence, CFEngine is a configuration management tool. The most important promise type is the files promise, which is used to configure a system through managing the content of configuration files. CFEngine should therefore be designed around this use case.

To avoid issues as a result of empty configuration files, the new behavior of files promises is as follows:

  • Files are created by default after rendering succeeds (for content / templates).
  • Specify create => "true" if you want the file to always be created.
  • Use if => fileexists("$(this.promiser)") to only edit files which exist (This has the same effect as create => "false", but the if is preferred).
  • The behavior of other files promises is unchanged (such as permissions, line editing, etc.)

Share your thoughts

If you want to play around with these new changes today; feel free to check out our packages from the master branch. For instructions on how, please see Installing CFEngine Nightlies using cf-remote. Please let us know what you think over at GitHub Discussions. We are eager to hear your feedback.