Add new methods to the technique editor

Use case

When some specifics use cases are not supported by Rudder, you may want to be able to create your own methods usable in the technique editor.

This document will go through the complete writing of a method that does not exist in the current Rudder library.

Planning

For this example we will want to create a method that let us define a Rudder variable based on the result of a template.

To do this we will use the pre-existing Mustache templating methods furnished in Rudder: * Mustache: it is Rudder’s native templating engine, with a very simple syntax, and the best performance. It should be the default choice for most cases.

Prerequisites

All our Linux methods are built on the same manner, using the underlying tool CFEngine. Some basic knowledge on CFEngine will be required to fully understand this guide. In Rudder we are using a slightly different vocabulary than CFEngine:

  • The classes are called conditions in Rudder

  • The bundle name is the method name

Create a technique editor understandable method

First we need to define a minimal bundle that will become our future new method. All methods are always bundles of the agent type. The bundle name must respects the [a-z_]+ syntax.

The bundle will take 3 parameters: * variable_prefix and variable_name to describe the full naming space of the variable we want to set. * template_path which is the path of the source template we will use.

Let’s create the file variable_string_from_mustache_file.cf in /var/rudder/configuration-repository/ncf/30_generic_methods/ containing:

bundle agent variable_string_from_mustache_file(variable_prefix, variable_name, template_path)
{
}

Method metadata

Then, let’s add some Rudder’s specific tags to the parameters, which are used by the technique editor to interpret the method.

# @name Variable string from mustache template
# @description Define a variable from an expanded mustache template
#
# @parameter variable_prefix The prefix of the variable name
# @parameter variable_name   The variable to define, the full name will be variable_prefix.variable_name
# @parameter template_path   The path of the mustache template file
#
# @class_prefix variable_string_from_mustache_template
# @class_parameter variable_name
bundle agent variable_string_from_mustache_file(variable_prefix, variable_name, template_path)
{
}
  • @name is obviously the method name, as it will be displayed in the technique editor

  • @description is a small description of the goal of the method

  • @documentation can be multiline and support the Markdown syntax

  • @parameter is used to define a small description of the linked parameter

    • The syntax is @parameter <parameter name> <description>

  • @class_prefix and class_parameter will define the common base of the resulting classes generated by the method.

    • The resulting classes will be variable_string_from_mustache_template_<class parameter>_<result status>

    • With:

      • <class parameter> being the value passed to the parameter set as the class_parameter

      • <result status> is either success|repaired|error

We now have a minimal method, which is understandable by the technique editor, but which can not report nor do anything.

Test it

To test it in the interface, we now need to commit the file in the Rudder configuration-repository and reload the Rudder techniques library.

cd /var/rudder/configuration-repository/ncf/30_generic_methods
git add variable_string_from_mustache_file.cf
git commit -m "Adding v0 of variable_string_from_mustache_file"
rudder server reload-techniques

Whenever you create new methods, to avoid upgrade conflict/outage, always place them in the /var/rudder/configuration-repository/ncf/30_generic_methods. And whenever you will need to modify one of those, commit the changes and reload the technique library.

You should now be able to see your new method in the technique editor.

Method core

Old class prefix

In Rudder we are always using the same base code to define the resulting classes with a variable named old_class_prefix, used to define the resulting classes used in the technique editor.

# Basically the building of our result class, without the status
"old_class_prefix"  string => canonify("<@class_prefix>_<@class_parameter>");
# In our example:
"old_class_prefix"  string => canonify("variable_string_from_mustache_template_${variable_name}");

Class prefix

Another variable class_prefix, used to differentiate the different calls to the same method. It basically is the old_class_prefix but build with the concatenation of all the args of the methods.

# args it a list of all the args
"args"              slist => { "${variable_prefix}", "${variable_name}", "${template_path}" };
# we concatenate them
"report_param"      string => join("_", args);
# make a complete class_prefix out of it
"full_class_prefix" string => canonify("variable_string_from_mustache_template_${report_param}");
#trunc it since super long conditions may cause some extra issues
"class_prefix"      string => string_head("${full_class_prefix}", "1000");

Reporting

Finally, the reporting is done by another module called _log_v3 which takes as parameters:

# Message is the common part that will be displayed in the Rudder report, it will be suffixed
# with the result status.

bundle agent _log_v3(message, class_parameter, old_class_prefix, class_prefix, args)

Since it is another bundle, we need to call it using a usebundle promise in methods sections.

Summary

At this point, we should have a method looking like this:

# Not showing the comments parts anymore
bundle agent variable_string_from_mustache_file(variable_prefix, variable_name, template_path)
{
  vars:
      "old_class_prefix"  string => canonify("variable_string_from_mustache_template_${variable_name}");
      "args"              slist => { "${variable_prefix}", "${variable_name}", "${template_path}" };
      "report_param"      string => join("_", args);
      "full_class_prefix" string => canonify("variable_string_from_mustache_template_${report_param}");
      "class_prefix"      string => string_head("${full_class_prefix}", "1000");

  methods:
      "report" usebundle  => _log_v3("Set the string ${variable_prefix}.${variable_name} to the mustache template expansion of ${template_path}", "${variable_name}", "${old_class_prefix}", "${class_prefix}", @{args});
}

Make our method do something

We can now start adding state changes in out code. The goal was to define a method to set a variable from the result of a Mustache template. To do that we need to:

  • Load the template file

  • Apply it

  • Create a variable from its result

  • Do some reporting

# Not showing the comments parts anymore
bundle agent variable_string_from_mustache_file(variable_prefix, variable_name, template_path)
{
  vars:
      "old_class_prefix"  string => canonify("variable_string_from_mustache_template_${variable_name}");
      "args"              slist => { "${variable_prefix}", "${variable_name}", "${template_path}" };
      "report_param"      string => join("_", args);
      "full_class_prefix" string => canonify("variable_string_from_mustache_template_${report_param}");
      "class_prefix"      string => string_head("${full_class_prefix}", "1000");

      # define the variable within the variable_prefix namespace
      "template"          string => readfile("${template_path}" , "0");           (1)
      "content"           string => string_mustache("${template}" , datastate()); (2)
      "${variable_prefix}.${variable_name}" string => "${content}";               (3)

  classes:
     "variable_defined"     expression => isvariable("${variable_prefix}.${variable_name}"); (4)

    !variable_defined::                                                 (5)
      "error"    usebundle => _classes_failure("${old_class_prefix}");
      "error"    usebundle => _classes_failure("${class_prefix}");

    variable_defined::
      "success"  usebundle => _classes_success("${old_class_prefix}");
      "success"  usebundle => _classes_success("${class_prefix}");

  methods:
      "report" usebundle  => _log_v3("Set the string ${variable_prefix}.${variable_name} to the mustache template expansion of ${template_path}", "${variable_name}", "${old_class_prefix}", "${class_prefix}", @{args});
}
1 Load the content of the template in template
2 Apply the template in the variable content
3 Assign its content to the goal variable <variable_prefix.variable_name>
4 Test that the goal variable is well defined, if yes, define the local condition variable_define <5>
  • If variable_defined exists, create the global result condition with a success status

  • If it is not defined, create the global result condition with an error status

You can now commit and try to use the method.

To go further

Now that we have a functional method, we could try to make it more bullet proof. * What if the template file does not exists * Or the template is malformed?

You can try to avoid this cases by checking the datastate before reporting, or by assigning a default value to our content variable before template expansion and compare it afterward.


← Use relays and server as repository mirrors Use secrets in configuration policies →