Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions api/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -916,10 +916,11 @@ func (wc *WaitConfig) Copy() *WaitConfig {
}

type ChangeScript struct {
Command *string `mapstructure:"command" hcl:"command"`
Args []string `mapstructure:"args" hcl:"args,optional"`
Timeout *time.Duration `mapstructure:"timeout" hcl:"timeout,optional"`
FailOnError *bool `mapstructure:"fail_on_error" hcl:"fail_on_error"`
Command *string `mapstructure:"command" hcl:"command"`
Args []string `mapstructure:"args" hcl:"args,optional"`
Timeout *time.Duration `mapstructure:"timeout" hcl:"timeout,optional"`
FailOnError *bool `mapstructure:"fail_on_error" hcl:"fail_on_error"`
RunOnFirstRender *bool `mapstructure:"run_on_first_render" hcl:"run_on_first_render,optional"`
}

func (ch *ChangeScript) Canonicalize() {
Expand All @@ -935,6 +936,9 @@ func (ch *ChangeScript) Canonicalize() {
if ch.FailOnError == nil {
ch.FailOnError = pointerOf(false)
}
if ch.RunOnFirstRender == nil {
ch.RunOnFirstRender = pointerOf(false)
}
}

type Template struct {
Expand Down
43 changes: 42 additions & 1 deletion client/allocrunner/taskrunner/template/template.go
Comment thread
VedantMadane marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ type TaskTemplateManager struct {
// shutdownCh is used to signal and started goroutine to shutdown
shutdownCh chan struct{}

// firstRenderScripts holds change_scripts that should fire after the
// initial template render once the task reaches the running state.
firstRenderScripts []*structs.ChangeScript

// shutdown marks whether the manager has been shutdown
shutdown bool
shutdownLock sync.Mutex
Expand Down Expand Up @@ -276,8 +280,13 @@ func (tm *TaskTemplateManager) Run() {
// Unblock the task
close(tm.config.UnblockCh)

// Collect change_script templates that should fire on first render.
// These are stored and executed later when the task reaches the running
// state, triggered via RunFirstRenderScripts from the Poststart hook.
tm.firstRenderScripts = tm.collectFirstRenderScripts()

// If all our templates are change mode no-op, then we can exit here
if tm.allTemplatesNoop() {
if tm.allTemplatesNoop() && len(tm.firstRenderScripts) == 0 {
return
}

Expand Down Expand Up @@ -646,6 +655,38 @@ func (tm *TaskTemplateManager) allTemplatesNoop() bool {
return true
}

// collectFirstRenderScripts returns the set of ChangeScript objects from
// templates that have change_mode "script" with RunOnFirstRender enabled.
func (tm *TaskTemplateManager) collectFirstRenderScripts() []*structs.ChangeScript {
var scripts []*structs.ChangeScript
for _, tmpl := range tm.config.Templates {
if tmpl.ChangeMode == structs.TemplateChangeModeScript &&
tmpl.ChangeScript != nil &&
tmpl.ChangeScript.RunOnFirstRender {
scripts = append(scripts, tmpl.ChangeScript)
}
}
return scripts
}

// RunFirstRenderScripts executes the change_scripts collected during the
// first template render. It is called by the template hook's Poststart
// method once the task is running and Exec is available.
func (tm *TaskTemplateManager) RunFirstRenderScripts() {
scripts := tm.firstRenderScripts
if len(scripts) == 0 {
return
}
tm.firstRenderScripts = nil

var wg sync.WaitGroup
for _, script := range scripts {
wg.Add(1)
go tm.processScript(script, &wg)
}
wg.Wait()
}

// templateRunner returns a consul-template runner for the given templates and a
// lookup by destination to the template. If no templates are in the config, a
// nil template runner and lookup is returned.
Expand Down
53 changes: 53 additions & 0 deletions client/allocrunner/taskrunner/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,59 @@ OUTER:
}
}

// TestTaskTemplateManager_FirstRenderScript verifies that a template with
// change_mode "script" and RunOnFirstRender collects the script so it can
// be executed once the task reaches the running state via RunFirstRenderScripts.
func TestTaskTemplateManager_FirstRenderScript(t *testing.T) {
ci.Parallel(t)
clienttestutil.RequireConsul(t)

key := "first_render_key"
t1 := &structs.Template{
EmbeddedTmpl: `FOO={{key "first_render_key"}}` + "\n",
DestPath: "first_render.env",
ChangeMode: structs.TemplateChangeModeScript,
ChangeScript: &structs.ChangeScript{
Command: "/bin/foo",
Args: []string{},
Timeout: 5 * time.Second,
FailOnError: false,
RunOnFirstRender: true,
},
Envvars: true,
}

harness := newTestHarness(t, []*structs.Template{t1}, true, false)
harness.mockHooks.SetupExecTest(0, nil)
harness.start(t)
defer harness.stop()

// Write key so the template renders
harness.consul.SetKV(t, key, []byte("hello"))

// Wait for unblock (first render complete)
select {
case <-harness.mockHooks.UnblockCh:
case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second):
t.Fatal("Task unblock should have been called")
}

// Simulate the Poststart hook by calling RunFirstRenderScripts directly
harness.mockHooks.HasHandle = true
harness.manager.RunFirstRenderScripts()

// Verify script execution event was emitted
timeout := time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second)
select {
case ev := <-harness.mockHooks.EmitEventCh:
if !strings.Contains(ev.DisplayMessage, t1.ChangeScript.Command) {
t.Fatalf("expected script event, got: %s", ev.DisplayMessage)
}
case <-timeout:
t.Fatal("should have received a script execution event")
}
}

// TestTaskTemplateManager_ScriptExecutionFailTask tests whether we fail the
// task upon script execution failure if that's how it's configured.
func TestTaskTemplateManager_ScriptExecutionFailTask(t *testing.T) {
Expand Down
10 changes: 10 additions & 0 deletions client/allocrunner/taskrunner/template_hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,16 @@ func (h *templateHook) newManager(tmpls []*structs.Template) (manager *template.
return m, unblock, nil
}

func (h *templateHook) Poststart(_ context.Context, _ *interfaces.TaskPoststartRequest, _ *interfaces.TaskPoststartResponse) error {
h.managerLock.Lock()
defer h.managerLock.Unlock()

if h.templateManager != nil {
h.templateManager.RunFirstRenderScripts()
}
return nil
}

func (h *templateHook) Stop(_ context.Context, req *interfaces.TaskStopRequest, resp *interfaces.TaskStopResponse) error {
h.managerLock.Lock()
defer h.managerLock.Unlock()
Expand Down
9 changes: 5 additions & 4 deletions command/agent/job_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -1553,10 +1553,11 @@ func apiChangeScriptToStructsChangeScript(changeScript *api.ChangeScript) *struc
}

return &structs.ChangeScript{
Command: *changeScript.Command,
Args: changeScript.Args,
Timeout: *changeScript.Timeout,
FailOnError: *changeScript.FailOnError,
Command: *changeScript.Command,
Args: changeScript.Args,
Timeout: *changeScript.Timeout,
FailOnError: *changeScript.FailOnError,
RunOnFirstRender: *changeScript.RunOnFirstRender,
}
}

Expand Down
13 changes: 7 additions & 6 deletions command/agent/job_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3097,12 +3097,13 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
EmbeddedTmpl: pointer.Of("embedded"),
ChangeMode: pointer.Of("change"),
ChangeSignal: pointer.Of("signal"),
ChangeScript: &api.ChangeScript{
Command: pointer.Of("/bin/foo"),
Args: []string{"-h"},
Timeout: pointer.Of(5 * time.Second),
FailOnError: pointer.Of(false),
},
ChangeScript: &api.ChangeScript{
Command: pointer.Of("/bin/foo"),
Args: []string{"-h"},
Timeout: pointer.Of(5 * time.Second),
FailOnError: pointer.Of(false),
RunOnFirstRender: pointer.Of(false),
},
Splay: pointer.Of(1 * time.Minute),
Perms: pointer.Of("666"),
Uid: pointer.Of(1000),
Expand Down
3 changes: 3 additions & 0 deletions jobspec2/parse_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,7 @@ func normalizeChangeScript(ch *api.ChangeScript) {
if ch.FailOnError == nil {
ch.FailOnError = pointerOf(false)
}
if ch.RunOnFirstRender == nil {
ch.RunOnFirstRender = pointerOf(false)
}
}
12 changes: 12 additions & 0 deletions nomad/structs/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8656,6 +8656,12 @@ func TestTaskDiff(t *testing.T) {
Old: "",
New: "false",
},
{
Type: DiffTypeAdded,
Name: "RunOnFirstRender",
Old: "",
New: "false",
},
{
Type: DiffTypeAdded,
Name: "Timeout",
Expand Down Expand Up @@ -8780,6 +8786,12 @@ func TestTaskDiff(t *testing.T) {
Old: "false",
New: "",
},
{
Type: DiffTypeDeleted,
Name: "RunOnFirstRender",
Old: "false",
New: "",
},
{
Type: DiffTypeDeleted,
Name: "Timeout",
Expand Down
15 changes: 11 additions & 4 deletions nomad/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8919,6 +8919,10 @@ type ChangeScript struct {
// FailOnError indicates whether a task should fail in case script execution
// fails or log script failure and don't interrupt the task
FailOnError bool
// RunOnFirstRender indicates that the script should also be triggered
// after the initial template render, not only on subsequent re-renders.
// The script will execute once the task reaches the running state.
RunOnFirstRender bool
}

func (cs *ChangeScript) Equal(o *ChangeScript) bool {
Expand All @@ -8934,6 +8938,8 @@ func (cs *ChangeScript) Equal(o *ChangeScript) bool {
return false
case cs.FailOnError != o.FailOnError:
return false
case cs.RunOnFirstRender != o.RunOnFirstRender:
return false
}
return true
}
Expand All @@ -8943,10 +8949,11 @@ func (cs *ChangeScript) Copy() *ChangeScript {
return nil
}
return &ChangeScript{
Command: cs.Command,
Args: slices.Clone(cs.Args),
Timeout: cs.Timeout,
FailOnError: cs.FailOnError,
Command: cs.Command,
Args: slices.Clone(cs.Args),
Timeout: cs.Timeout,
FailOnError: cs.FailOnError,
RunOnFirstRender: cs.RunOnFirstRender,
}
}

Expand Down
12 changes: 8 additions & 4 deletions nomad/structs/structs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7143,10 +7143,11 @@ func TestChangeScript_Equal(t *testing.T) {
must.NotEqual[*ChangeScript](t, nil, new(ChangeScript))

must.StructEqual(t, &ChangeScript{
Command: "/bin/sleep",
Args: []string{"infinity"},
Timeout: 1 * time.Second,
FailOnError: true,
Command: "/bin/sleep",
Args: []string{"infinity"},
Timeout: 1 * time.Second,
FailOnError: true,
RunOnFirstRender: true,
}, []must.Tweak[*ChangeScript]{{
Field: "Command",
Apply: func(c *ChangeScript) { c.Command = "/bin/false" },
Expand All @@ -7159,6 +7160,9 @@ func TestChangeScript_Equal(t *testing.T) {
}, {
Field: "FailOnError",
Apply: func(c *ChangeScript) { c.FailOnError = false },
}, {
Field: "RunOnFirstRender",
Apply: func(c *ChangeScript) { c.RunOnFirstRender = false },
}})
}

Expand Down