Building Reusable Modules with Conditionals
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:
- You can’t nest conditionals
- You can’t define params inside of a branch
- You can’t load modules inside of a branch
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:
- Dealing with different operating systems and platforms
- Dealing with user-configurable options
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.