Skip to content

Commit c7e87a1

Browse files
authored
Add "randInt" template function for random number (#540)
The `randInt` function added by this change returns a pseudo-random number in a given range and using a seed for repeatable randomness. It can be used, for example, in cron schedules to pick a minute offset based on an environment variable (e.g. hostname).
1 parent 461710b commit c7e87a1

File tree

3 files changed

+31
-0
lines changed

3 files changed

+31
-0
lines changed

docs/content/configuration/templates.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,9 @@ resticprofile supports the following set of own functions in all templates:
600600
* `{{ tempDir }}` => `/tmp/resticprofile.../t` - unique OS specific existing temporary directory
601601
* `{{ tempFile "filename" }}` => `/tmp/resticprofile.../t/filename` - unique OS specific existing temporary file
602602
* `{{ env }}` => `/tmp/resticprofile.../t/profile.env` - unique OS specific existing temporary file that is added to the current profile env-files list
603+
* `{{ "seed" | randInt low high }}` => `123` -
604+
Generate a random number greater than or equal to `low` and less than `high`,
605+
using the value of `seed` for repeatable randomness.
603606

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

util/templates/functions.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package templates
22

33
import (
4+
"crypto/md5" //nolint:gosec
45
"encoding/base64"
6+
"encoding/binary"
57
"encoding/hex"
68
"errors"
79
"fmt"
810
"maps"
11+
"math/rand/v2"
912
"os"
1013
"path"
1114
"path/filepath"
@@ -43,6 +46,7 @@ import (
4346
// - {{ "plain" | base64 }} => "cGxhaW4="
4447
// - {{ tempDir }} => "/path/to/unique-tempdir"
4548
// - {{ tempFile "filename" }} => "/path/to/unique-tempdir/filename"
49+
// - {{ "seed" | randInt 123 456 }} => 166
4650
func TemplateFuncs(funcs ...map[string]any) (templateFuncs map[string]any) {
4751
templateFuncs = map[string]any{
4852
"contains": func(search any, src any) bool { return strings.Contains(toString(src), toString(search)) },
@@ -64,6 +68,7 @@ func TemplateFuncs(funcs ...map[string]any) (templateFuncs map[string]any) {
6468
"tempDir": TempDir,
6569
"tempFile": TempFile,
6670
"env": func() string { return TempFile(".env.none") }, // satisfies the {{env}} interface w.o. functionality
71+
"randInt": randInt,
6772
}
6873

6974
// aliases
@@ -214,3 +219,22 @@ func toMap(args ...any) (m map[string]any) {
214219
}
215220
return
216221
}
222+
223+
// randInt uses the seed to initialize a pseudo-random number generator and
224+
// returns a number between low (inclusive) and high (exclusive).
225+
func randInt(low, high int64, seed string) int64 {
226+
if low >= high {
227+
panic(fmt.Sprintf("low (%d) must be less than high (%d)", low, high))
228+
}
229+
230+
// MD5 produces the right amount of output bytes and isn't used for
231+
// cryptography.
232+
sum := md5.Sum([]byte(seed)) //nolint:gosec
233+
234+
seed1 := binary.LittleEndian.Uint64(sum[0:8])
235+
seed2 := binary.LittleEndian.Uint64(sum[8:16])
236+
237+
r := rand.New(rand.NewPCG(seed1, seed2))
238+
239+
return low + r.Int64N(high-low)
240+
}

util/templates/functions_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ func TestTemplateFuncs(t *testing.T) {
5959
{template: `{{ "plain" | hex }}`, expected: "706c61696e"},
6060
{template: `{{ "plain" | base64 }}`, expected: "cGxhaW4="},
6161
{template: `{{ hello }}`, expected: `Hello World`},
62+
{template: `{{ "" | randInt 0 1 }}`, expected: `0`},
63+
{template: `{{ "hello" | randInt 5 6 }}`, expected: `5`},
64+
{template: `{{ "" | randInt -1000 1000 }}`, expected: `419`},
65+
{template: `{{ "ABC" | randInt -1000 1000 }}`, expected: `-272`},
6266
}
6367

6468
extraFuncs := map[string]any{

0 commit comments

Comments
 (0)