Templates

Templates are a great way to compose configuration profiles.

Please keep in mind that yaml files are sensitive to the number of spaces. Also if you declare a block already declared, it overrides the previous declaration (instead of merging them).

For that matter, configuration templates are probably more useful if you use the toml or hcl configuration format.

Here’s a simple example

{{ define "hello" }}
hello = "world"
{{ end }}

To use the content of this template anywhere in your configuration, simply call it:

{{ template "hello" . }}

Note the dot after the name: it’s used to pass the variables to the template. Without it, all your variables (like .Profile.Name) would display <no value>.

Here’s a working example:

#
# This is an example of configuration using nested templates
#

# nested template declarations
# this template declaration won't appear here in the configuration file
# it will only appear when called by { { template "backup_root" . } }
{{ define "backup_root" }}
    exclude = [ "{{ .Profile.Name }}-backup.log" ]
    exclude-file = [
        "{{ .ConfigDir }}/root-excludes",
        "{{ .ConfigDir }}/excludes"
    ]
    exclude-caches = true
    tag = [ "root" ]
    source = [ "/" ]
{{ end }}

version = "1"

[global]
  priority = "low"
  ionice = true
  ionice-class = 2
  ionice-level = 6

[base]
  status-file = "{{ .Env.HOME }}/status.json"

    [base.snapshots]
      host = true

    [base.retention]
      host = true
      after-backup = true
      keep-within = "30d"

#########################################################

[nas]
  inherit = "base"
  repository = "rest:http://{{ .Env.BACKUP_REST_USER }}:{{ .Env.BACKUP_REST_PASSWORD }}@nas:8000/root"
  password-file = "nas-key"

# root

[nas-root]
  inherit = "nas"

    [nas-root.backup]
      # get the content of "backup_root" defined at the top
      {{ template "backup_root" . }}
      schedule = "01:47"
      schedule-permission = "system"
      schedule-log = "{{ .Profile.Name }}-backup.log"

#########################################################

[azure]
  inherit = "base"
  repository = "azure:restic:/"
  password-file = "azure-key"
  lock = "/tmp/resticprofile-azure.lock"

    [azure.backup]
      schedule-permission = "system"
      schedule-log = "{{ .Profile.Name }}-backup.log"

# root

[azure-root]
  inherit = "azure"

    [azure-root.backup]
      # get the content of "backup_root" defined at the top
      {{ template "backup_root" . }}
      schedule = "03:58"

# mysql

[azure-mysql]
  inherit = "azure"

    [azure-mysql.backup]
      tag = [ "mysql" ]
      run-before = [
          "rm -f /tmp/mysqldumpall.sql",
          "mysqldump -u{{ .Env.MYSQL_BACKUP_USER }} -p{{ .Env.MYSQL_BACKUP_PASSWORD }} --all-databases > /tmp/mysqldumpall.sql"
      ]
      source = "/tmp/mysqldumpall.sql"
      run-after = [
          "rm -f /tmp/mysqldumpall.sql"
      ]
      schedule = "03:18"
#
# This is an example of configuration using nested templates
#

# nested template declarations
# this template declaration won't appear here in the configuration file
# it will only appear when called by { { template "backup_root" . } }

{{ define "backup_root" }}
    exclude:
      - '{{ .Profile.Name }}-backup.log'
    exclude-file:
      - '{{ .ConfigDir }}/root-excludes'
      - '{{ .ConfigDir }}/excludes'
    exclude-caches: true
    tag:
      - root
    source:
      - /
{{ end }}

version: "1"

global:
  priority: low
  ionice: true
  ionice-class: 2
  ionice-level: 6

base:
  status-file: '{{ .Env.HOME }}/status.json'
  snapshots:
    host: true
  retention:
    host: true
    after-backup: true
    keep-within: 30d

nas:
  inherit: base
  repository: >-
    rest:http://{{ .Env.BACKUP_REST_USER }}:{{ .Env.BACKUP_REST_PASSWORD
    }}@nas:8000/root    
  password-file: nas-key

nas-root:
  inherit: nas
  backup:
    # get the content of "backup_root" defined at the top
{{ template "backup_root" . }}
    schedule: '01:47'
    schedule-permission: system
    schedule-log: '{{ .Profile.Name }}-backup.log'

azure:
  inherit: base
  repository: 'azure:restic:/'
  password-file: azure-key
  lock: /tmp/resticprofile-azure.lock
  backup:
    schedule-permission: system
    schedule-log: '{{ .Profile.Name }}-backup.log'

azure-root:
  inherit: azure
  backup:
    # get the content of "backup_root" defined at the top
{{ template "backup_root" . }}
    schedule: '03:58'

azure-mysql:
  inherit: azure
  backup:
    tag:
      - mysql
    run-before:
      - rm -f /tmp/mysqldumpall.sql
      - >-
        mysqldump -u{{ .Env.MYSQL_BACKUP_USER }} -p{{ .Env.MYSQL_BACKUP_PASSWORD
        }} --all-databases > /tmp/mysqldumpall.sql        
    source: /tmp/mysqldumpall.sql
    run-after:
      - rm -f /tmp/mysqldumpall.sql
    schedule: '03:18'
#
# This is an example of configuration using nested templates
#

# nested template declarations
# this template declaration won't appear here in the configuration file
# it will only appear when called by { { template "backup_root" . } }

{{ define "backup_root" }}
  "exclude" = ["{{ .Profile.Name }}-backup.log"]
  "exclude-file" = ["{{ .ConfigDir }}/root-excludes", "{{ .ConfigDir }}/excludes"]
  "exclude-caches" = true
  "tag" = ["root"]
  "source" = ["/"]
{{end}}

"global" = {
  "priority" = "low"
  "ionice" = true
  "ionice-class" = 2
  "ionice-level" = 6
}

"base" = {
  "status-file" = "{{ .Env.HOME }}/status.json"

  "snapshots" = {
    "host" = true
  }

  "retention" = {
    "host" = true
    "after-backup" = true
    "keep-within" = "30d"
  }
}

"nas" = {
  "inherit" = "base"
  "repository" = "rest:http://{{ .Env.BACKUP_REST_USER }}:{{ .Env.BACKUP_REST_PASSWORD }}@nas:8000/root"
  "password-file" = "nas-key"
}

"nas-root" = {
  "inherit" = "nas"

  "backup" = {
    # get the content of "backup_root" defined at the top
    {{ template "backup_root" . }}
    "schedule" = "01:47"
    "schedule-permission" = "system"
    "schedule-log" = "{{ .Profile.Name }}-backup.log"
  }
}

"azure" = {
  "inherit" = "base"
  "repository" = "azure:restic:/"
  "password-file" = "azure-key"
  "lock" = "/tmp/resticprofile-azure.lock"

  "backup" = {
    "schedule-permission" = "system"
    "schedule-log" = "{{ .Profile.Name }}-backup.log"
  }
}

"azure-root" = {
  "inherit" = "azure"

  "backup" = {
    # get the content of "backup_root" defined at the top
    {{ template "backup_root" . }}
    "schedule" = "03:58"
  }
}

"azure-mysql" = {
  "inherit" = "azure"

  "backup" = {
    "tag" = ["mysql"]
    "run-before" = ["rm -f /tmp/mysqldumpall.sql", "mysqldump -u{{ .Env.MYSQL_BACKUP_USER }} -p{{ .Env.MYSQL_BACKUP_PASSWORD }} --all-databases > /tmp/mysqldumpall.sql"]
    "source" = "/tmp/mysqldumpall.sql"
    "run-after" = ["rm -f /tmp/mysqldumpall.sql"]
    "schedule" = "03:18"
  }
}
{{ define "backup_root" }}
    "exclude": [
      "{{ .Profile.Name }}-backup.log"
    ],
    "exclude-file": [
      "{{ .ConfigDir }}/root-excludes",
      "{{ .ConfigDir }}/excludes"
    ],
    "exclude-caches": true,
    "tag": [
      "root"
    ],
    "source": [
      "/"
    ],
{{ end }}
{
  "version": "1",
  "global": {
    "priority": "low",
    "ionice": true,
    "ionice-class": 2,
    "ionice-level": 6
  },
  "base": {
    "status-file": "{{ .Env.HOME }}/status.json",
    "snapshots": {
      "host": true
    },
    "retention": {
      "host": true,
      "after-backup": true,
      "keep-within": "30d"
    }
  },
  "nas": {
    "inherit": "base",
    "repository": "rest:http://{{ .Env.BACKUP_REST_USER }}:{{ .Env.BACKUP_REST_PASSWORD }}@nas:8000/root",
    "password-file": "nas-key"
  },
  "nas-root": {
    "inherit": "nas",
    "backup": {
      {{ template "backup_root" . }}
      "schedule": "01:47",
      "schedule-permission": "system",
      "schedule-log": "{{ .Profile.Name }}-backup.log"
    }
  },
  "azure": {
    "inherit": "base",
    "repository": "azure:restic:/",
    "password-file": "azure-key",
    "lock": "/tmp/resticprofile-azure.lock",
    "backup": {
      "schedule-permission": "system",
      "schedule-log": "{{ .Profile.Name }}-backup.log"
    }
  },
  "azure-root": {
    "inherit": "azure",
    "backup": {
      {{ template "backup_root" . }}
      "schedule": "03:58"
    }
  },
  "azure-mysql": {
    "inherit": "azure",
    "backup": {
      "tag": [
        "mysql"
      ],
      "run-before": [
        "rm -f /tmp/mysqldumpall.sql",
        "mysqldump -u{{ .Env.MYSQL_BACKUP_USER }} -p{{ .Env.MYSQL_BACKUP_PASSWORD }} --all-databases > /tmp/mysqldumpall.sql"
      ],
      "source": "/tmp/mysqldumpall.sql",
      "run-after": [
        "rm -f /tmp/mysqldumpall.sql"
      ],
      "schedule": "03:18"
    }
  }
}

Debugging your template and variable expansion

If for some reason you don’t understand why resticprofile is not loading your configuration file, you can display the generated configuration after executing the template (and replacing the variables and everything) using the --trace flag. We will see it in action in a moment.

Limitations of using templates

There’s something to be aware of when dealing with templates: at the time the template is compiled, it has no knowledge of what the end configuration should look like: it has no knowledge of profiles for example. Here is a non-working example of what I mean:

version = "1"

{{ define "retention" }}
  [{{ .Profile.Name }}.retention]
    after-backup = true
    before-backup = false
    compact = false
    keep-within = "30d"
    prune = true
{{ end }}

[src]
  password-file = "{{ .ConfigDir }}/{{ .Profile.Name }}-key"
  repository = "/backup/{{ .Now.Weekday }}"
  lock = "$HOME/resticprofile-profile-{{ .Profile.Name }}.lock"
  initialize = true

    [src.backup]
      source = "{{ .Env.HOME }}/go/src"
      check-before = true
      exclude = ["/**/.git"]
      exclude-caches = true
      tag = ["{{ .Profile.Name }}", "dev"]

    {{ template "retention" . }}

    [src.snapshots]
      tag = ["{{ .Profile.Name }}", "dev"]

[other]
  password-file = "{{ .ConfigDir }}/{{ .Profile.Name }}-key"
  repository = "/backup/{{ .Now.Weekday }}"
  lock = "$HOME/resticprofile-profile-{{ .Profile.Name }}.lock"
  initialize = true

    {{ template "retention" . }}

Here we define a template retention that we use twice. When you ask for a configuration of a profile, either src or other the template will change all occurrences of { .Profile.Name } to the name of the profile, no matter where it is inside the file.

% resticprofile -c examples/parse-error.toml -n src show
2020/11/06 21:39:48 cannot load configuration file: cannot parse toml configuration: While parsing config: (35, 6): duplicated tables
exit status 1

Run the command again, this time asking a display of the compiled version of the configuration:

% resticprofile -c examples/parse-error.toml -n src --trace show
2020/11/06 21:48:20 resticprofile 0.10.0-dev compiled with go1.15.3
2020/11/06 21:48:20 Resulting configuration for profile 'default':
====================
  1:
  2:
  3: [src]
  4: password-file = "/Users/CP/go/src/resticprofile/examples/default-key"
  5: repository = "/backup/Friday"
  6: lock = "$HOME/resticprofile-profile-default.lock"
  7: initialize = true
  8:
  9:     [src.backup]
 10:     source = "/Users/CP/go/src"
 11:     check-before = true
 12:     exclude = ["/**/.git"]
 13:     exclude-caches = true
 14:     tag = ["default", "dev"]
 15:
 16:
 17:     [default.retention]
 18:     after-backup = true
 19:     before-backup = false
 20:     compact = false
 21:     keep-within = "30d"
 22:     prune = true
 23:
 24:
 25:     [src.snapshots]
 26:     tag = ["default", "dev"]
 27:
 28: [other]
 29: password-file = "/Users/CP/go/src/resticprofile/examples/default-key"
 30: repository = "/backup/Friday"
 31: lock = "$HOME/resticprofile-profile-default.lock"
 32: initialize = true
 33:
 34:
 35:     [default.retention]
 36:     after-backup = true
 37:     before-backup = false
 38:     compact = false
 39:     keep-within = "30d"
 40:     prune = true
 41:
 42:
====================
2020/11/06 21:48:20 cannot load configuration file: cannot parse toml configuration: While parsing config: (35, 6): duplicated tables
exit status 1

As you can see in lines 17 and 35, there are 2 sections of the same name. They could be both called [src.retention], but actually the reason why they’re both called [default.retention] is that resticprofile is doing a first pass to load the [global] section using a profile name of default.

The fix for this configuration is very simple though, just remove the section name from the template:

{{ define "retention" }}
    after-backup = true
    before-backup = false
    compact = false
    keep-within = "30d"
    prune = true
{{ end }}

version = "1"

[src]
  password-file = "{{ .ConfigDir }}/{{ .Profile.Name }}-key"
  repository = "/backup/{{ .Now.Weekday }}"
  lock = "$HOME/resticprofile-profile-{{ .Profile.Name }}.lock"
  initialize = true

    [src.backup]
      source = "{{ .Env.HOME }}/go/src"
      check-before = true
      exclude = ["/**/.git"]
      exclude-caches = true
      tag = ["{{ .Profile.Name }}", "dev"]

    [src.retention]
      {{ template "retention" . }}

    [src.snapshots]
      tag = ["{{ .Profile.Name }}", "dev"]

[other]
  password-file = "{{ .ConfigDir }}/{{ .Profile.Name }}-key"
  repository = "/backup/{{ .Now.Weekday }}"
  lock = "$HOME/resticprofile-profile-{{ .Profile.Name }}.lock"
  initialize = true

    [other.retention]
      {{ template "retention" . }}

And now you no longer end up with duplicated sections.

Documentation on template, variable expansion and other configuration scripting

There are a lot more you can do with configuration templates. If you’re brave enough, you can read the full documentation of the Go templates.

For a more end-user kind of documentation, you can also read hugo documentation on templates which is using the same Go implementation, but don’t talk much about the developer side of it. Please note there are some functions only made available by hugo though, resticprofile adds its own set of functions.

Template functions

resticprofile supports the following set of own functions in all templates:

  • {{ "some string" | contains "some" }} => true
  • {{ "some string" | matches "^.+str.+$" }} => true
  • {{ "some old string" | replace "old" "new" }} => some new string
  • {{ "some old string" | replaceR "(old)" "$1 and new" }} => some old and new string
  • {{ "some old string" | regex "(old)" "$1 and new" }} => some old and new string (regex is an alias to replaceR)
  • {{ "ABC" | lower }} => abc
  • {{ "abc" | upper }} => ABC
  • {{ " A " | trim }} => A
  • {{ "--A-" | trimPrefix "--" }} => A-
  • {{ "--A-" | trimSuffix "-" }} => --A
  • {{ range $v := "A,B,C" | split "," }} ({{ $v }}) {{ end }} => (A) (B) (C)
  • {{ "A,B,C" | split "," | join ";" }} => A;B;C
  • {{ "A, B, C" | splitR "\\s*,\\s*" | join ";" }} => A;B;C
  • {{ range $v := list "A" "B" "C" }} ({{ $v }}) {{ end }} => (A) (B) (C)
  • {{ with map "k1" "v1" "k2" "v2" }} {{ .k1 }}-{{ .k2 }} {{ end }} => v1-v2
  • {{ with list "A" "B" "C" "D" | map }} {{ ._0 }}-{{ ._1 }}-{{ ._3 }} {{ end }} => A-B-D
  • {{ with list "A" "B" "C" "D" | map "key" }} {{ .key | join "-" }} {{ end }} => A-B-C-D
  • {{ tempDir }} => /tmp/resticprofile.../t - unique OS specific existing temporary directory
  • {{ tempFile "filename" }} => /tmp/resticprofile.../t/filename - unique OS specific existing temporary file

All {{ temp* }} functions guarantee that returned temporary directories and files are existing & writable. When resticprofile ends, temporary directories and files are removed.

The following functions can be used to encode data (e.g. when dealing with arbitrary input):

  • {{ "a & b\n" | js }} => a \u0026 b\u000A - JSON string equivalent of the input (builtin)
  • {{ "a & b\n" | html }} => a &amp; b\n - HTML text escaped input (builtin)
  • {{ "a & b\n" | urlquery }} => a+%26+b%0A - URL query escaped input (builtin)
  • {{ "plain" | base64 }} => cGxhaW4= - Base64 encoded input
  • {{ "plain" | hex }} => 706c61696e - Hexadecimal encoded input

Encode with js when creating strings in YAML, TOML or JSON configuration files, e.g.: "{{ .Env.MyVar | js }}". This ensures the markup remains correct and can be parsed regardless of the input data.

Please refer to the official documentation for the general syntax and set of default functions provided in go text templates.