Working with Templates

Globus Compute endpoints often need different configurations for different environments, workflows, or users (see Multi-User Endpoints). Rather than maintaining multiple endpoints or configuration files, endpoint administrators can create a single configuration template to generate customized configurations based on variables provided during task submission.

Familiarity with Jinja template concepts is a prerequisite to this guide, which focuses on the unique features and peculiarities of Globus Compute configuration templates.

Testing Templates

The Compute Endpoint CLI includes a tool, globus-compute-endpoint render-user-config, which can be used to test the template rendering process without having to submit a task or even configure an endpoint.

Note

For the most up-to-date documentation on available options, run globus-compute-endpoint render-user-config --help.

In its simplest mode, calling the tool might look like this:

Example usage with local endpoint
$ globus-compute-endpoint render-user-config -e my_endpoint -o user_options.json

> Rendered user config:
engine:
   type: GlobusComputeEngine
   provider:
      type: LocalProvider
      worker_init: "echo 'hello world'"
~/.globus_compute/my_endpoint/user_config_template.yaml.j2
engine:
   type: GlobusComputeEngine
   provider:
      type: LocalProvider
      worker_init: {{ worker_init }}
user_options.json
{ "worker_init": "echo 'hello world'" }

The -e option, short for --endpoint, takes a local endpoint name/UUID, and the -o option, short for --user-options, takes a filename pointing to JSON data. Here, the tool reads the endpoint’s config, renders the associated template with any values in user_options.json included, and outputs the rendered template to stdout.

Alternatively, the render tool can be pointed directly to the template file, along with any other files it might need:

Example “offline” usage without an endpoint
$ cat user_options.json | globus-compute-endpoint render-user-config \
   --template user_config_template.yaml.j2 \
   --user-options-file - \
   <other options...>

This applies the same rendering logic and also outputs the final value to stdout as before - the main difference is that this approach does not require a pre-configured endpoint. Note that the - argument used here for --user-options-file indicates that the file should be read from stdin, which works with any other file-based options as well.

These approaches can also be mixed. Whenever the --endpoint option is present, file paths are pulled from the endpoint’s config by default, but any file paths supplied as options take precedence. This makes it easy to A/B test changes without modifying files in production.

Testing Reserved Variables

When rendering reserved variables, the render tool makes reasonable guesses and supplies dummy data. In cases where admins want more control, they can specify the value(s) to render to any reserved variable:

Custom user_runtime
$ globus-compute-endpoint render-user-config -t template-snippet.yaml.j2 \
   --user-runtime my-custom-user-runtime.json

> Rendered user config:
worker_init: |
   echo 'Globus Compute SDK version: "arbitrary value"'
   echo 'foo: "bar"'
template-snippet.yaml.j2
worker_init: |
   echo 'Globus Compute SDK version: {{ user_runtime.globus_compute_sdk_version }}'
   echo 'foo: {{ user_runtime.foo }}'
my-custom-user-runtime.json
{
   "globus_compute_sdk_version": "arbitrary value",
   "foo": "bar"
}

Validating YAML Syntax

When working with Jinja (or any templating engine for that matter), it’s easy to accidentally produce syntactically invalid output. Since the render tool outputs the rendered config to stdout and all other information to stderr, admins can pipe to another tool for handling validation:

Using yq for validation
$ globus-compute-endpoint render-user-config \
   -t bad-template.yaml.j2 \
   | yq - >/dev/null

> Rendered user config:
Error: bad file '-': yaml: line 2: mapping values are not allowed in this context
bad-template.yaml.j2
engine: there shouldn't be any text here!
   type: GlobusComputeEngine

This example leverages yq to validate the rendered YAML, which prints syntax errors to stderr. Specifically, this invocation takes its input from stdin with the - argument, and silences its output (usually just the input YAML repeated) by redirecting to /dev/null.

User Input Security

To prevent injection attacks, Globus Compute JSON-serializes all user-defined strings before rendering the template. This adds double quotes to the string, which will require special handling in some scenarios.

For example, let’s imagine we want our configuration to change depending on the value of a user-defined environment variable:

user_config_template.yaml.j2
engine:
  type: GlobusComputeEngine
  provider:
    type: SlurmProvider
{% if environment == '"test"' %}
    partition: test
    worker_init: "source test_setup.sh"
    walltime: 00:01:00
 {% else %}
    partition: default
    worker_init: "source normal_setup.sh"
    walltime: 00:30:00
 {% endif %}

Note that we are using the full JSON-serialized string for the if-condition. The template would fail to render if we used 'test' or "test" instead of '"test"'.

Reserved Variables

Every template has access to reserved variables that cannot be overridden by the user or admin:

  • parent_config: Contains the configuration values of the parent manager endpoint. This can be helpful in situations involving Python-based configuration files.

  • user_runtime: Contains information about the runtime environment of the user submitting tasks, such as their Python version. The following fields are available:

    • globus_compute_sdk_version: Version string of the Globus Compute SDK (e.g., "3.14.0")

    • globus_sdk_version: Version string of the base Globus SDK dependency (e.g., "3.63.0")

    • python: Contains information about the Python runtime. The following fields are available:

      • version: Python version string (e.g., "3.13.7")

      • version_description: Full Python version string from sys.version (e.g., "3.13.7 (main, Jul  1 2024, 12:00:00) [Clang 14.0.6]")

      • version_tuple: Python version tuple as a list (e.g., [3, 13, 7])

      • version_info: Python version info from list(sys.version_info) (e.g., [3, 13, 7, 'final', 0])

      • implementation: Python implementation (e.g., "CPython")

      • compiler: String identifying the compiler used for compiling Python (e.g., "Clang 14.0.6")

    • platform: Contains general platform information from the host. The following fields are available:

      • architecture: Host architecture tuple as a list (e.g., ["64bit", "ELF"])

      • machine: Host machine type (e.g., "x86_64")

      • node: Host node name (e.g., "login03")

      • platform: Host platform (e.g., "Linux-6.14.0-29-generic-x86_64-with-glibc2.39")

      • processor: Host processor name (e.g., "x86_64")

      • release: Host OS release (e.g., "6.16.5-2-generic")

  • mapped_identity: Contains information about the user’s mapped identity. The following fields are available:

    • local.uname: Local user’s username

    • local.uid: Local user’s ID

    • local.gid: Local user’s primary group ID

    • local.groups: List of group IDs the local user is a member of

    • local.gecos: Local user’s GECOS field

    • local.shell: Local user’s login shell

    • local.dir: Local user’s home directory

    • globus.id: Matched Globus identity ID

    Example usage of mapped_identity in a template
     engine:
        type: GlobusComputeEngine
        provider:
           type: SlurmProvider
     {% if 1001 in mapped_identity.local.groups %}
           partition: {{ partition }}
     {% else %}
           partition: default
     {% endif %}
    

Combining Templates

Endpoint administrators can combine multiple templates with the extends, include, and import Jinja tags. However, since these templates are rendered in user space, the administrator must:

  1. Move the template files to a directory that every mapped local user account has read access to.

  2. Specify the main template file path with the user_config_template_path variable in the endpoint manager configuration.

Example usage of extends and include in a template
{% extends "base_config.yaml" %}

provider:
  type: SlurmProvider

{% if environment == '"test"' %}
{% include "test_config.yaml" %}
{% else %}
{% include "default_config.yaml" %}
{% endif %}