Skip to content

Commit 4989466

Browse files
committed
template: add RunOnFirstRender option to change_script
Add a RunOnFirstRender field to the ChangeScript configuration that allows template change_script to fire after the initial template render, not only on subsequent re-renders. When enabled, the template manager waits for the task to reach the running state after unblocking, then executes the configured scripts. This is useful for initialization tasks that need to run once the first template content is available. Fixes #27429
1 parent 9e6d492 commit 4989466

8 files changed

Lines changed: 103 additions & 23 deletions

File tree

api/tasks.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -916,10 +916,11 @@ func (wc *WaitConfig) Copy() *WaitConfig {
916916
}
917917

918918
type ChangeScript struct {
919-
Command *string `mapstructure:"command" hcl:"command"`
920-
Args []string `mapstructure:"args" hcl:"args,optional"`
921-
Timeout *time.Duration `mapstructure:"timeout" hcl:"timeout,optional"`
922-
FailOnError *bool `mapstructure:"fail_on_error" hcl:"fail_on_error"`
919+
Command *string `mapstructure:"command" hcl:"command"`
920+
Args []string `mapstructure:"args" hcl:"args,optional"`
921+
Timeout *time.Duration `mapstructure:"timeout" hcl:"timeout,optional"`
922+
FailOnError *bool `mapstructure:"fail_on_error" hcl:"fail_on_error"`
923+
RunOnFirstRender *bool `mapstructure:"run_on_first_render" hcl:"run_on_first_render,optional"`
923924
}
924925

925926
func (ch *ChangeScript) Canonicalize() {
@@ -935,6 +936,9 @@ func (ch *ChangeScript) Canonicalize() {
935936
if ch.FailOnError == nil {
936937
ch.FailOnError = pointerOf(false)
937938
}
939+
if ch.RunOnFirstRender == nil {
940+
ch.RunOnFirstRender = pointerOf(false)
941+
}
938942
}
939943

940944
type Template struct {

client/allocrunner/taskrunner/template/template.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,16 @@ func (tm *TaskTemplateManager) Run() {
276276
// Unblock the task
277277
close(tm.config.UnblockCh)
278278

279+
// Collect any change_script templates that should fire on first render.
280+
// These scripts are launched in a goroutine because they need the task
281+
// to reach the running state before Exec can succeed.
282+
firstRenderScripts := tm.collectFirstRenderScripts()
283+
if len(firstRenderScripts) > 0 {
284+
go tm.runFirstRenderScripts(firstRenderScripts)
285+
}
286+
279287
// If all our templates are change mode no-op, then we can exit here
280-
if tm.allTemplatesNoop() {
288+
if tm.allTemplatesNoop() && len(firstRenderScripts) == 0 {
281289
return
282290
}
283291

@@ -646,6 +654,46 @@ func (tm *TaskTemplateManager) allTemplatesNoop() bool {
646654
return true
647655
}
648656

657+
// collectFirstRenderScripts returns the set of ChangeScript objects from
658+
// templates that have change_mode "script" with RunOnFirstRender enabled.
659+
func (tm *TaskTemplateManager) collectFirstRenderScripts() []*structs.ChangeScript {
660+
var scripts []*structs.ChangeScript
661+
for _, tmpl := range tm.config.Templates {
662+
if tmpl.ChangeMode == structs.TemplateChangeModeScript &&
663+
tmpl.ChangeScript != nil &&
664+
tmpl.ChangeScript.RunOnFirstRender {
665+
scripts = append(scripts, tmpl.ChangeScript)
666+
}
667+
}
668+
return scripts
669+
}
670+
671+
// runFirstRenderScripts waits for the task to reach the running state and then
672+
// executes the given change_scripts. It respects the shutdown channel so it
673+
// will not block forever if the task is killed before reaching running.
674+
func (tm *TaskTemplateManager) runFirstRenderScripts(scripts []*structs.ChangeScript) {
675+
const pollInterval = 500 * time.Millisecond
676+
ticker := time.NewTicker(pollInterval)
677+
defer ticker.Stop()
678+
679+
for {
680+
select {
681+
case <-tm.shutdownCh:
682+
return
683+
case <-ticker.C:
684+
if tm.config.Lifecycle.IsRunning() {
685+
var wg sync.WaitGroup
686+
for _, script := range scripts {
687+
wg.Add(1)
688+
go tm.processScript(script, &wg)
689+
}
690+
wg.Wait()
691+
return
692+
}
693+
}
694+
}
695+
}
696+
649697
// templateRunner returns a consul-template runner for the given templates and a
650698
// lookup by destination to the template. If no templates are in the config, a
651699
// nil template runner and lookup is returned.

command/agent/job_endpoint.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,10 +1553,11 @@ func apiChangeScriptToStructsChangeScript(changeScript *api.ChangeScript) *struc
15531553
}
15541554

15551555
return &structs.ChangeScript{
1556-
Command: *changeScript.Command,
1557-
Args: changeScript.Args,
1558-
Timeout: *changeScript.Timeout,
1559-
FailOnError: *changeScript.FailOnError,
1556+
Command: *changeScript.Command,
1557+
Args: changeScript.Args,
1558+
Timeout: *changeScript.Timeout,
1559+
FailOnError: *changeScript.FailOnError,
1560+
RunOnFirstRender: *changeScript.RunOnFirstRender,
15601561
}
15611562
}
15621563

command/agent/job_endpoint_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3097,12 +3097,13 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
30973097
EmbeddedTmpl: pointer.Of("embedded"),
30983098
ChangeMode: pointer.Of("change"),
30993099
ChangeSignal: pointer.Of("signal"),
3100-
ChangeScript: &api.ChangeScript{
3101-
Command: pointer.Of("/bin/foo"),
3102-
Args: []string{"-h"},
3103-
Timeout: pointer.Of(5 * time.Second),
3104-
FailOnError: pointer.Of(false),
3105-
},
3100+
ChangeScript: &api.ChangeScript{
3101+
Command: pointer.Of("/bin/foo"),
3102+
Args: []string{"-h"},
3103+
Timeout: pointer.Of(5 * time.Second),
3104+
FailOnError: pointer.Of(false),
3105+
RunOnFirstRender: pointer.Of(false),
3106+
},
31063107
Splay: pointer.Of(1 * time.Minute),
31073108
Perms: pointer.Of("666"),
31083109
Uid: pointer.Of(1000),

jobspec2/parse_job.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,7 @@ func normalizeChangeScript(ch *api.ChangeScript) {
163163
if ch.FailOnError == nil {
164164
ch.FailOnError = pointerOf(false)
165165
}
166+
if ch.RunOnFirstRender == nil {
167+
ch.RunOnFirstRender = pointerOf(false)
168+
}
166169
}

nomad/structs/diff_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8656,6 +8656,12 @@ func TestTaskDiff(t *testing.T) {
86568656
Old: "",
86578657
New: "false",
86588658
},
8659+
{
8660+
Type: DiffTypeAdded,
8661+
Name: "RunOnFirstRender",
8662+
Old: "",
8663+
New: "false",
8664+
},
86598665
{
86608666
Type: DiffTypeAdded,
86618667
Name: "Timeout",
@@ -8780,6 +8786,12 @@ func TestTaskDiff(t *testing.T) {
87808786
Old: "false",
87818787
New: "",
87828788
},
8789+
{
8790+
Type: DiffTypeDeleted,
8791+
Name: "RunOnFirstRender",
8792+
Old: "false",
8793+
New: "",
8794+
},
87838795
{
87848796
Type: DiffTypeDeleted,
87858797
Name: "Timeout",

nomad/structs/structs.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8919,6 +8919,10 @@ type ChangeScript struct {
89198919
// FailOnError indicates whether a task should fail in case script execution
89208920
// fails or log script failure and don't interrupt the task
89218921
FailOnError bool
8922+
// RunOnFirstRender indicates that the script should also be triggered
8923+
// after the initial template render, not only on subsequent re-renders.
8924+
// The script will execute once the task reaches the running state.
8925+
RunOnFirstRender bool
89228926
}
89238927

89248928
func (cs *ChangeScript) Equal(o *ChangeScript) bool {
@@ -8934,6 +8938,8 @@ func (cs *ChangeScript) Equal(o *ChangeScript) bool {
89348938
return false
89358939
case cs.FailOnError != o.FailOnError:
89368940
return false
8941+
case cs.RunOnFirstRender != o.RunOnFirstRender:
8942+
return false
89378943
}
89388944
return true
89398945
}
@@ -8943,10 +8949,11 @@ func (cs *ChangeScript) Copy() *ChangeScript {
89438949
return nil
89448950
}
89458951
return &ChangeScript{
8946-
Command: cs.Command,
8947-
Args: slices.Clone(cs.Args),
8948-
Timeout: cs.Timeout,
8949-
FailOnError: cs.FailOnError,
8952+
Command: cs.Command,
8953+
Args: slices.Clone(cs.Args),
8954+
Timeout: cs.Timeout,
8955+
FailOnError: cs.FailOnError,
8956+
RunOnFirstRender: cs.RunOnFirstRender,
89508957
}
89518958
}
89528959

nomad/structs/structs_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7143,10 +7143,11 @@ func TestChangeScript_Equal(t *testing.T) {
71437143
must.NotEqual[*ChangeScript](t, nil, new(ChangeScript))
71447144

71457145
must.StructEqual(t, &ChangeScript{
7146-
Command: "/bin/sleep",
7147-
Args: []string{"infinity"},
7148-
Timeout: 1 * time.Second,
7149-
FailOnError: true,
7146+
Command: "/bin/sleep",
7147+
Args: []string{"infinity"},
7148+
Timeout: 1 * time.Second,
7149+
FailOnError: true,
7150+
RunOnFirstRender: true,
71507151
}, []must.Tweak[*ChangeScript]{{
71517152
Field: "Command",
71527153
Apply: func(c *ChangeScript) { c.Command = "/bin/false" },
@@ -7159,6 +7160,9 @@ func TestChangeScript_Equal(t *testing.T) {
71597160
}, {
71607161
Field: "FailOnError",
71617162
Apply: func(c *ChangeScript) { c.FailOnError = false },
7163+
}, {
7164+
Field: "RunOnFirstRender",
7165+
Apply: func(c *ChangeScript) { c.RunOnFirstRender = false },
71627166
}})
71637167
}
71647168

0 commit comments

Comments
 (0)