diff --git a/.changelog/13755.txt b/.changelog/13755.txt new file mode 100644 index 000000000000..3b1c2c051ae5 --- /dev/null +++ b/.changelog/13755.txt @@ -0,0 +1,3 @@ +```release-note:improvement +template: Templates support new uid/gid parameter pair +``` \ No newline at end of file diff --git a/api/jobs_test.go b/api/jobs_test.go index 6cc8b3abf17b..b585a6ac9b77 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -765,6 +765,8 @@ func TestJobs_Canonicalize(t *testing.T) { ChangeSignal: stringToPtr(""), Splay: timeToPtr(5 * time.Second), Perms: stringToPtr("0644"), + Uid: intToPtr(0), + Gid: intToPtr(0), LeftDelim: stringToPtr("{{"), RightDelim: stringToPtr("}}"), Envvars: boolToPtr(false), @@ -778,6 +780,8 @@ func TestJobs_Canonicalize(t *testing.T) { ChangeSignal: stringToPtr(""), Splay: timeToPtr(5 * time.Second), Perms: stringToPtr("0644"), + Uid: intToPtr(0), + Gid: intToPtr(0), LeftDelim: stringToPtr("{{"), RightDelim: stringToPtr("}}"), Envvars: boolToPtr(true), diff --git a/api/tasks.go b/api/tasks.go index 6cdb44da3a3b..6e38620df47e 100644 --- a/api/tasks.go +++ b/api/tasks.go @@ -799,6 +799,8 @@ type Template struct { ChangeSignal *string `mapstructure:"change_signal" hcl:"change_signal,optional"` Splay *time.Duration `mapstructure:"splay" hcl:"splay,optional"` Perms *string `mapstructure:"perms" hcl:"perms,optional"` + Uid *int `mapstructure:"uid" hcl:"uid,optional"` + Gid *int `mapstructure:"gid" hcl:"gid,optional"` LeftDelim *string `mapstructure:"left_delimiter" hcl:"left_delimiter,optional"` RightDelim *string `mapstructure:"right_delimiter" hcl:"right_delimiter,optional"` Envvars *bool `mapstructure:"env" hcl:"env,optional"` @@ -835,6 +837,12 @@ func (tmpl *Template) Canonicalize() { if tmpl.Perms == nil { tmpl.Perms = stringToPtr("0644") } + if tmpl.Uid == nil { + tmpl.Uid = intToPtr(0) + } + if tmpl.Gid == nil { + tmpl.Gid = intToPtr(0) + } if tmpl.LeftDelim == nil { tmpl.LeftDelim = stringToPtr("{{") } diff --git a/client/allocrunner/taskrunner/template/template.go b/client/allocrunner/taskrunner/template/template.go index 37c3f1da6eba..3d4308c707d9 100644 --- a/client/allocrunner/taskrunner/template/template.go +++ b/client/allocrunner/taskrunner/template/template.go @@ -626,6 +626,12 @@ func parseTemplateConfigs(config *TaskTemplateManagerConfig) (map[*ctconf.Templa m := os.FileMode(v) ct.Perms = &m } + // Set ownership + if tmpl.Uid >= 0 && tmpl.Gid >= 0 { + ct.Uid = &tmpl.Uid + ct.Gid = &tmpl.Gid + } + ct.Finalize() ctmpls[ct] = tmpl diff --git a/client/allocrunner/taskrunner/template/template_test.go b/client/allocrunner/taskrunner/template/template_test.go index 0221cfe1ca53..2c679ba1fae0 100644 --- a/client/allocrunner/taskrunner/template/template_test.go +++ b/client/allocrunner/taskrunner/template/template_test.go @@ -16,6 +16,7 @@ import ( "strconv" "strings" "sync" + "syscall" "testing" "time" @@ -33,6 +34,7 @@ import ( sconfig "github.com/hashicorp/nomad/nomad/structs/config" "github.com/hashicorp/nomad/testutil" "github.com/kr/pretty" + "github.com/shoenig/test/must" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -512,6 +514,8 @@ func TestTaskTemplateManager_Permissions(t *testing.T) { DestPath: file, ChangeMode: structs.TemplateChangeModeNoop, Perms: "777", + Uid: 503, + Gid: 20, } harness := newTestHarness(t, []*structs.Template{template}, false, false) @@ -535,6 +539,13 @@ func TestTaskTemplateManager_Permissions(t *testing.T) { if m := fi.Mode(); m != os.ModePerm { t.Fatalf("Got mode %v; want %v", m, os.ModePerm) } + + sys := fi.Sys() + uid := int(sys.(*syscall.Stat_t).Uid) + gid := int(sys.(*syscall.Stat_t).Gid) + + must.Eq(t, template.Uid, uid) + must.Eq(t, template.Gid, gid) } func TestTaskTemplateManager_Unblock_Static_NomadEnv(t *testing.T) { diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index f7a74c4f8d07..9c85e4b1cdfd 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -1209,6 +1209,14 @@ func ApiTaskToStructsTask(job *structs.Job, group *structs.TaskGroup, if len(apiTask.Templates) > 0 { structsTask.Templates = []*structs.Template{} for _, template := range apiTask.Templates { + uid := -1 + if template.Uid != nil { + uid = *template.Uid + } + gid := -1 + if template.Gid != nil { + gid = *template.Gid + } structsTask.Templates = append(structsTask.Templates, &structs.Template{ SourcePath: *template.SourcePath, @@ -1218,6 +1226,8 @@ func ApiTaskToStructsTask(job *structs.Job, group *structs.TaskGroup, ChangeSignal: *template.ChangeSignal, Splay: *template.Splay, Perms: *template.Perms, + Uid: uid, + Gid: gid, LeftDelim: *template.LeftDelim, RightDelim: *template.RightDelim, Envvars: *template.Envvars, diff --git a/command/agent/job_endpoint_test.go b/command/agent/job_endpoint_test.go index 28e08ed31813..bb30f91fb2e6 100644 --- a/command/agent/job_endpoint_test.go +++ b/command/agent/job_endpoint_test.go @@ -2733,6 +2733,8 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) { ChangeSignal: helper.StringToPtr("signal"), Splay: helper.TimeToPtr(1 * time.Minute), Perms: helper.StringToPtr("666"), + Uid: helper.IntToPtr(1000), + Gid: helper.IntToPtr(1000), LeftDelim: helper.StringToPtr("abc"), RightDelim: helper.StringToPtr("def"), Envvars: helper.BoolToPtr(true), @@ -3138,6 +3140,8 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) { ChangeSignal: "SIGNAL", Splay: 1 * time.Minute, Perms: "666", + Uid: 1000, + Gid: 1000, LeftDelim: "abc", RightDelim: "def", Envvars: true, diff --git a/jobspec/helper.go b/jobspec/helper.go index 99bf80f830f1..826cba0652ba 100644 --- a/jobspec/helper.go +++ b/jobspec/helper.go @@ -18,6 +18,11 @@ func stringToPtr(str string) *string { return &str } +// intToPtr returns the pointer to an int +func intToPtr(i int) *int { + return &i +} + // timeToPtr returns the pointer to a time.Duration. func timeToPtr(t time.Duration) *time.Duration { return &t diff --git a/jobspec/helper_test.go b/jobspec/helper_test.go deleted file mode 100644 index f7854c80195b..000000000000 --- a/jobspec/helper_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package jobspec - -// These functions are copied from helper/funcs.go -// added here to avoid jobspec depending on any other package - -// intToPtr returns the pointer to an int -func intToPtr(i int) *int { - return &i -} - -// int8ToPtr returns the pointer to an int8 -func int8ToPtr(i int8) *int8 { - return &i -} - -// int64ToPtr returns the pointer to an int -func int64ToPtr(i int64) *int64 { - return &i -} - -// Uint64ToPtr returns the pointer to an uint64 -func uint64ToPtr(u uint64) *uint64 { - return &u -} diff --git a/jobspec/parse_task.go b/jobspec/parse_task.go index 4bc77c310f27..87299146719a 100644 --- a/jobspec/parse_task.go +++ b/jobspec/parse_task.go @@ -441,6 +441,8 @@ func parseTemplates(result *[]*api.Template, list *ast.ObjectList) error { "destination", "left_delimiter", "perms", + "uid", + "gid", "right_delimiter", "source", "splay", @@ -460,6 +462,8 @@ func parseTemplates(result *[]*api.Template, list *ast.ObjectList) error { ChangeMode: stringToPtr("restart"), Splay: timeToPtr(5 * time.Second), Perms: stringToPtr("0644"), + Uid: intToPtr(0), + Gid: intToPtr(0), } dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index 45d624aa22f1..fba7bb62c84d 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -24,6 +24,17 @@ const ( templateChangeModeRestart = "restart" ) +// Helper functions below are only used by this test suite +func int8ToPtr(i int8) *int8 { + return &i +} +func uint64ToPtr(u uint64) *uint64 { + return &u +} +func int64ToPtr(i int64) *int64 { + return &i +} + func TestParse(t *testing.T) { ci.Parallel(t) @@ -363,6 +374,8 @@ func TestParse(t *testing.T) { ChangeSignal: stringToPtr("foo"), Splay: timeToPtr(10 * time.Second), Perms: stringToPtr("0644"), + Uid: intToPtr(0), + Gid: intToPtr(0), Envvars: boolToPtr(true), VaultGrace: timeToPtr(33 * time.Second), }, @@ -372,6 +385,8 @@ func TestParse(t *testing.T) { ChangeMode: stringToPtr(templateChangeModeRestart), Splay: timeToPtr(5 * time.Second), Perms: stringToPtr("777"), + Uid: intToPtr(1001), + Gid: intToPtr(20), LeftDelim: stringToPtr("--"), RightDelim: stringToPtr("__"), }, diff --git a/jobspec/test-fixtures/basic.hcl b/jobspec/test-fixtures/basic.hcl index 20a8171e4d00..273ae6ebdbf2 100644 --- a/jobspec/test-fixtures/basic.hcl +++ b/jobspec/test-fixtures/basic.hcl @@ -318,6 +318,8 @@ job "binstore-storagelocker" { source = "bar" destination = "bar" perms = "777" + uid = 1001 + gid = 20 left_delimiter = "--" right_delimiter = "__" } diff --git a/jobspec2/helper_test.go b/jobspec2/helper_test.go deleted file mode 100644 index 57a6cd36028d..000000000000 --- a/jobspec2/helper_test.go +++ /dev/null @@ -1,5 +0,0 @@ -package jobspec2 - -func intToPtr(v int) *int { - return &v -} diff --git a/jobspec2/parse_job.go b/jobspec2/parse_job.go index 9b533874f500..de19fe40936c 100644 --- a/jobspec2/parse_job.go +++ b/jobspec2/parse_job.go @@ -107,6 +107,12 @@ func normalizeTemplates(templates []*api.Template) { if t.Perms == nil { t.Perms = stringToPtr("0644") } + if t.Uid == nil { + t.Uid = intToPtr(0) + } + if t.Gid == nil { + t.Gid = intToPtr(0) + } if t.Splay == nil { t.Splay = durationToPtr(5 * time.Second) } @@ -121,6 +127,10 @@ func boolToPtr(v bool) *bool { return &v } +func intToPtr(v int) *int { + return &v +} + func stringToPtr(v string) *string { return &v } diff --git a/nomad/structs/diff_test.go b/nomad/structs/diff_test.go index 1a5751a8c7e7..d081a72bed54 100644 --- a/nomad/structs/diff_test.go +++ b/nomad/structs/diff_test.go @@ -7044,6 +7044,8 @@ func TestTaskDiff(t *testing.T) { ChangeSignal: "SIGHUP", Splay: 1, Perms: "0644", + Uid: 1001, + Gid: 21, Wait: &WaitConfig{ Min: helper.TimeToPtr(5 * time.Second), Max: helper.TimeToPtr(5 * time.Second), @@ -7057,6 +7059,8 @@ func TestTaskDiff(t *testing.T) { ChangeSignal: "SIGHUP2", Splay: 2, Perms: "0666", + Uid: 1000, + Gid: 20, Envvars: true, }, }, @@ -7071,6 +7075,8 @@ func TestTaskDiff(t *testing.T) { ChangeSignal: "SIGHUP", Splay: 1, Perms: "0644", + Uid: 1001, + Gid: 21, Wait: &WaitConfig{ Min: helper.TimeToPtr(5 * time.Second), Max: helper.TimeToPtr(10 * time.Second), @@ -7084,6 +7090,8 @@ func TestTaskDiff(t *testing.T) { ChangeSignal: "SIGHUP3", Splay: 3, Perms: "0776", + Uid: 1002, + Gid: 22, Wait: &WaitConfig{ Min: helper.TimeToPtr(5 * time.Second), Max: helper.TimeToPtr(10 * time.Second), @@ -7154,6 +7162,12 @@ func TestTaskDiff(t *testing.T) { Old: "", New: "false", }, + { + Type: DiffTypeAdded, + Name: "Gid", + Old: "", + New: "22", + }, { Type: DiffTypeAdded, Name: "Perms", @@ -7172,6 +7186,12 @@ func TestTaskDiff(t *testing.T) { Old: "", New: "3", }, + { + Type: DiffTypeAdded, + Name: "Uid", + Old: "", + New: "1002", + }, { Type: DiffTypeAdded, Name: "VaultGrace", @@ -7234,6 +7254,12 @@ func TestTaskDiff(t *testing.T) { Old: "true", New: "", }, + { + Type: DiffTypeDeleted, + Name: "Gid", + Old: "20", + New: "", + }, { Type: DiffTypeDeleted, Name: "Perms", @@ -7252,6 +7278,12 @@ func TestTaskDiff(t *testing.T) { Old: "2", New: "", }, + { + Type: DiffTypeDeleted, + Name: "Uid", + Old: "1000", + New: "", + }, { Type: DiffTypeDeleted, Name: "VaultGrace", diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 800531e1b412..43596f5eb57b 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -7644,6 +7644,9 @@ type Template struct { // Perms is the permission the file should be written out with. Perms string + // User and group that should own the file. + Uid int + Gid int // LeftDelim and RightDelim are optional configurations to control what // delimiter is utilized when parsing the template. diff --git a/website/content/api-docs/json-jobs.mdx b/website/content/api-docs/json-jobs.mdx index 0223d38887ec..ffc7c89a116a 100644 --- a/website/content/api-docs/json-jobs.mdx +++ b/website/content/api-docs/json-jobs.mdx @@ -1077,6 +1077,20 @@ README][ct]. - `Perms` - Specifies the rendered template's permissions. File permissions are given as octal of the Unix file permissions `rwxrwxrwx`. +- `Uid` - Specifies the rendered template owner's user ID. + + ~> **Caveat:** Works only on Unix-based systems. Be careful when using + containerized drivers, suck as `docker` or `podman`, as groups and users + inside the container may have different IDs than on the host system. This + feature will also **not** work with Docker Desktop. + +- `Gid` - Specifies the rendered template owner's group ID. + + ~> **Caveat:** Works only on Unix-based systems. Be careful when using + containerized drivers, suck as `docker` or `podman`, as groups and users + inside the container may have different IDs than on the host system. This + feature will also **not** work with Docker Desktop. + - `RightDelim` - Specifies the right delimiter to use in the template. The default is "}}" for some templates, it may be easier to use a different delimiter that does not conflict with the output file itself. diff --git a/website/content/docs/job-specification/template.mdx b/website/content/docs/job-specification/template.mdx index 670f977950cc..8704bce7b1c2 100644 --- a/website/content/docs/job-specification/template.mdx +++ b/website/content/docs/job-specification/template.mdx @@ -84,6 +84,20 @@ refer to the [Learn Go Template Syntax][gt_learn] Learn guide. - `perms` `(string: "644")` - Specifies the rendered template's permissions. File permissions are given as octal of the Unix file permissions `rwxrwxrwx`. +- `uid` `(int: 0)` - Specifies the rendered template owner's user ID. + + ~> **Caveat:** Works only on Unix-based systems. Be careful when using + containerized drivers, suck as `docker` or `podman`, as groups and users + inside the container may have different IDs than on the host system. This + feature will also **not** work with Docker Desktop. + +- `gid` `(int: 0)` - Specifies the rendered template owner's group ID. + + ~> **Caveat:** Works only on Unix-based systems. Be careful when using + containerized drivers, suck as `docker` or `podman`, as groups and users + inside the container may have different IDs than on the host system. This + feature will also **not** work with Docker Desktop. + - `right_delimiter` `(string: "}}")` - Specifies the right delimiter to use in the template. The default is "}}" for some templates, it may be easier to use a different delimiter that does not conflict with the output file itself. @@ -561,4 +575,4 @@ options](/docs/configuration/client#options): [task working directory]: /docs/runtime/environment#task-directories 'Task Directories' [filesystem internals]: /docs/concepts/filesystem#templates-artifacts-and-dispatch-payloads [`client.template.wait_bounds`]: /docs/configuration/client#wait_bounds -[rhash]: https://en.wikipedia.org/wiki/Rendezvous_hashing \ No newline at end of file +[rhash]: https://en.wikipedia.org/wiki/Rendezvous_hashing