Asteris

Building Reusable Modules with Conditionals

// Rebecca Skinner

Converge 0.3.0 adds support for conditionals, making it easy to create a module that can react to different underlying operating systems, user configurations, and other runtime information.

Conditionals Overview

In this article we’re going to take a brief look at how conditionals in converge work. After we understand how they work at a high level, we’ll dig into how we can use them to build reusable modules.

Writing Switch Statements

If you’ve used switch statements in other languages conditionals in converge should look familiar. We’ve looked at C#, Java, and Javascript and tried to make conditionals that are familiar to people who know those languages.

Creating a switch statement in converge requires creating a named switch block. Inside of that block you can add named conditional branches with case or default. Below is a simple example that will use the platform module to display a greeting based on your current operating system.

greeting.hcl

switch "greeting" {
  case "eq `darwin` `{{platform.OS}}`" "macOS" {
    task.query "hello" {
      query = "echo 'hello, Mac user'"
    }
  }

  case "eq `linux` `{{platform.OS}}`" "linux" {
    task.query "hello" {
      query = "echo 'hello, Linux user'"
    }
  }

  default {
    task.query "hello" {
      query = "echo hello"
    }
  }
}

Predicates used in case statements are go text templates. Templates must evaluate to the string t or true (case-insensitive) to be considered true.

You can put as many resources inside of a branch as you want, and the names can overlap between branches without introducing conflicts. Limitations to keep in mind are:

There are also a few restrictions imposed on calls to lookup when dealing with branches.

Running Modules With Switch Statements

The engine will prefix “macro.” to the front of switch and branch nodes. This helps ensure there are no naming conflicts for automatically generated nodes.

Here’s an example of how the nodes are renamed:

converge plan --local hello.hcl

root/macro.switch.greeting/macro.case.linux/task.query.hello:
 Messages:
 Has Changes: no
 Changes: No changes

root/macro.switch.greeting/macro.case.default:
 Messages:
 Has Changes: no
 Changes: No changes

root/macro.switch.greeting/macro.case.linux:
 Messages:
 Has Changes: no
 Changes: No changes

root/macro.switch.greeting/macro.case.macOS/task.query.hello:
 Messages:
  check (returned: 0)
  hello, Mac user


 Has Changes: no
 Changes: No changes

root/macro.switch.greeting/macro.case.macOS:
 Messages:
 Has Changes: no
 Changes: No changes

root/macro.switch.greeting:
 Messages:
 Has Changes: no
 Changes: No changes

root/macro.switch.greeting/macro.case.default/task.query.hello:
 Messages:
 Has Changes: no
 Changes: No changes

Summary: 0 errors, 0 changes

In the output we can see that the switch node has been renamed to macro.switch.greeting. Each of the branches have also been renamed.

You may notice that each of the branches is visible, even though only one should have executed. Converge will generate a node for every possible branch.

You can see here that because I am using macOS, only the version of task.query "hello" that was defined in that branch has been executed.

Writing Conditionals

There are two general situations where using conditionals will help make your modules reusable:

To make a module reusable you need to start with finding identifying things that might need to differ. The difference could be due to platform, user configuration, or the result of some task. Since you can’t depend on things inside of a branch from outside of it, you need to move dependent nodes into branches too.

It’s important to keep node duplication within branches to a minimum. Duplication of nodes inside of branches can make it difficult to understand the execution graph. One way to avoid unnecessary duplication is to create modules for a subset of your branches. You can use converge graph to visualize the differences between the two branches. This can make it easier to know where to begin branches to have the fewest possible number of conditional nodes.

If the conditional nodes must happen early in the execution graph you may also add a dependency on the switch statement itself. In the example below we illustrate this by ensuring that we will write a file first if the save-message parameter is true.

param "save-message" {
  default = false
}

switch "example" {
  case "{{param `save-message`}}" "print message" {
    file.content "message" {
      destination = "message.txt"
      content = "Hello, World!"
    }
  }
}

task.query "after" {
  query = "echo Finished!"
  depends = ["macro.switch.example"]
}

Try it Out

A more detailed description of how to use conditionals can be found in the Getting Started Guide. You can get converge from github, or install the latest stable version with curl get.converge.sh | sh. Download it and try writing your own reusable modules; you can always ask questions and get help on our slack.