Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

template: custom change_mode scripts #13972

Merged
merged 71 commits into from
Aug 24, 2022
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
aa4e3a4
TemplateChangeModeScript
pkazmierczak Aug 3, 2022
cb79fb1
updated documentation
pkazmierczak Aug 4, 2022
cf3dffa
changelog entry
pkazmierczak Aug 4, 2022
03e6dab
wip
pkazmierczak Aug 4, 2022
3f2b1ec
handle script execution
pkazmierczak Aug 4, 2022
634e2d4
documentation
pkazmierczak Aug 4, 2022
f7a53cb
timeout can just be time.Duration
pkazmierczak Aug 4, 2022
56d0113
tasks & jobspec update
pkazmierczak Aug 4, 2022
efbd9cf
typos
pkazmierczak Aug 4, 2022
fbee6a2
adjusted tests
pkazmierczak Aug 5, 2022
5aa40b5
wip
pkazmierczak Aug 8, 2022
a8fcaba
corrected diff_test
pkazmierczak Aug 8, 2022
a2dd176
applied Derek's comments
pkazmierczak Aug 8, 2022
ae15843
corrections
pkazmierczak Aug 9, 2022
f9b12c6
revamped tests
pkazmierczak Aug 9, 2022
55d33da
revamped test
pkazmierczak Aug 9, 2022
611738b
Refactor test to listen for event channel
DerekStrickland Aug 9, 2022
1277dd6
diffs corrected
pkazmierczak Aug 10, 2022
ac8d910
CI-driven debugging :/
pkazmierczak Aug 10, 2022
2270765
fixed test
pkazmierczak Aug 10, 2022
8d61a25
FailTask
pkazmierczak Aug 10, 2022
4839c67
s/suck/such;
pkazmierczak Aug 10, 2022
5f98282
jobs_test correction
pkazmierczak Aug 11, 2022
8a6d591
documentation
pkazmierczak Aug 11, 2022
21f15ad
typo in the docs
pkazmierczak Aug 11, 2022
1f8b044
test FailTask
pkazmierczak Aug 11, 2022
b8f2ba8
Merge branch 'main' into f-nomad-template-custom-change-mode
pkazmierczak Aug 11, 2022
bcd7213
fail_on_error
pkazmierczak Aug 11, 2022
de9c7c2
addressed Derek's comments
pkazmierczak Aug 11, 2022
986a018
updated json-jobs.mdx
pkazmierczak Aug 11, 2022
df9f13b
typo in diff_test
pkazmierczak Aug 11, 2022
6ada97e
removed obsolete documentation
pkazmierczak Aug 11, 2022
d8fd77b
better canonicalization
pkazmierczak Aug 11, 2022
4673f95
make timeout optional
pkazmierczak Aug 11, 2022
f8f4fdd
hcl parser fixes
pkazmierczak Aug 11, 2022
3b68123
canonicalize test isn't needed
pkazmierczak Aug 11, 2022
87fd05f
pointer non-sense
pkazmierczak Aug 12, 2022
3d1ebdc
fixed conditional
pkazmierczak Aug 12, 2022
675a667
set poststart for template hook
pkazmierczak Aug 16, 2022
ce6f351
nil check
pkazmierczak Aug 16, 2022
9c25095
fixed test
pkazmierczak Aug 16, 2022
13b6f75
fixed tests
pkazmierczak Aug 16, 2022
2266453
Update website/content/api-docs/json-jobs.mdx
pkazmierczak Aug 18, 2022
010d8a1
Update website/content/api-docs/json-jobs.mdx
pkazmierczak Aug 18, 2022
ee50b9d
Update website/content/api-docs/json-jobs.mdx
pkazmierczak Aug 18, 2022
f834e8d
Merge branch 'main' into f-nomad-template-custom-change-mode
pkazmierczak Aug 18, 2022
de7156f
"path" documentation
pkazmierczak Aug 18, 2022
726397c
fail task in the poststart if template change mode is script and driv…
pkazmierczak Aug 18, 2022
67eda32
change script config -> change script
pkazmierczak Aug 18, 2022
0553d09
removed obsolete ioutil dependency
pkazmierczak Aug 18, 2022
a63d77c
execute scripts concurrently
pkazmierczak Aug 18, 2022
b4d4ec8
mutex for driver handle
pkazmierczak Aug 18, 2022
c97ea30
refactored onTemplateRendered
pkazmierczak Aug 18, 2022
944eeee
path -> command
pkazmierczak Aug 18, 2022
4b7b306
another script example in the docs
pkazmierczak Aug 19, 2022
b22d74f
simplified script execution processing
pkazmierczak Aug 19, 2022
f1050e1
handle exit code
pkazmierczak Aug 19, 2022
6190282
fix diff_test
pkazmierczak Aug 19, 2022
953c091
allowed_exit_codes
pkazmierczak Aug 19, 2022
4add2b0
Revert "allowed_exit_codes"
pkazmierczak Aug 19, 2022
f9daf96
simplified processScript
pkazmierczak Aug 19, 2022
b0e7181
Apply suggestions from code review
pkazmierczak Aug 23, 2022
1da69c1
Merge branch 'main' into f-nomad-template-custom-change-mode
pkazmierczak Aug 23, 2022
b9d0da2
refactored processScript
pkazmierczak Aug 23, 2022
d8c4cda
test fix
pkazmierczak Aug 23, 2022
a2a855e
nolint
pkazmierczak Aug 23, 2022
9629e69
removed obsolete code
pkazmierczak Aug 23, 2022
b77da29
Apply suggestions from code review
pkazmierczak Aug 24, 2022
0a1b5d7
Update website/content/api-docs/json-jobs.mdx
pkazmierczak Aug 24, 2022
88993bb
formatting
pkazmierczak Aug 24, 2022
96856ba
removed obsolete code
pkazmierczak Aug 24, 2022
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
3 changes: 3 additions & 0 deletions .changelog/13972.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
template: add script change_mode that allows scripts to be executed on template change
```
54 changes: 40 additions & 14 deletions api/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -791,21 +791,44 @@ func (wc *WaitConfig) Copy() *WaitConfig {
return nwc
}

type ChangeScriptConfig struct {
Path *string `mapstructure:"path" hcl:"path"`
pkazmierczak marked this conversation as resolved.
Show resolved Hide resolved
Args *[]string `mapstructure:"args" hcl:"args,optional"`
pkazmierczak marked this conversation as resolved.
Show resolved Hide resolved
Timeout *time.Duration `mapstructure:"timeout" hcl:"timeout,optional"`
FailOnError *bool `mapstructure:"fail_on_error" hcl:"fail_on_error"`
}

func (ch *ChangeScriptConfig) Canonicalize() {
if ch.Path == nil {
ch.Path = stringToPtr("")
}
if ch.Args == nil {
ch.Args = &[]string{}
}
if ch.Timeout == nil {
ch.Timeout = timeToPtr(5 * time.Second)
}
if ch.FailOnError == nil {
ch.FailOnError = boolToPtr(false)
}
}

type Template struct {
SourcePath *string `mapstructure:"source" hcl:"source,optional"`
DestPath *string `mapstructure:"destination" hcl:"destination,optional"`
EmbeddedTmpl *string `mapstructure:"data" hcl:"data,optional"`
ChangeMode *string `mapstructure:"change_mode" hcl:"change_mode,optional"`
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"`
VaultGrace *time.Duration `mapstructure:"vault_grace" hcl:"vault_grace,optional"`
Wait *WaitConfig `mapstructure:"wait" hcl:"wait,block"`
SourcePath *string `mapstructure:"source" hcl:"source,optional"`
DestPath *string `mapstructure:"destination" hcl:"destination,optional"`
EmbeddedTmpl *string `mapstructure:"data" hcl:"data,optional"`
ChangeMode *string `mapstructure:"change_mode" hcl:"change_mode,optional"`
ChangeScriptConfig *ChangeScriptConfig `mapstructure:"change_script_config" hcl:"change_script_config,block"`
pkazmierczak marked this conversation as resolved.
Show resolved Hide resolved
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"`
VaultGrace *time.Duration `mapstructure:"vault_grace" hcl:"vault_grace,optional"`
Wait *WaitConfig `mapstructure:"wait" hcl:"wait,block"`
}

func (tmpl *Template) Canonicalize() {
Expand All @@ -831,6 +854,9 @@ func (tmpl *Template) Canonicalize() {
sig := *tmpl.ChangeSignal
tmpl.ChangeSignal = stringToPtr(strings.ToUpper(sig))
}
if tmpl.ChangeScriptConfig != nil {
tmpl.ChangeScriptConfig.Canonicalize()
}
if tmpl.Splay == nil {
tmpl.Splay = timeToPtr(5 * time.Second)
}
Expand Down
32 changes: 32 additions & 0 deletions client/allocrunner/taskrunner/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ type TaskTemplateManagerConfig struct {

// NomadToken is the Nomad token or identity claim for the task
NomadToken string

// Handle is used to execute scripts
Handle interfaces.ScriptExecutor
}

// Validate validates the configuration.
Expand Down Expand Up @@ -392,6 +395,7 @@ func (tm *TaskTemplateManager) onTemplateRendered(handledRenders map[string]time

var handling []string
signals := make(map[string]struct{})
scripts := []*structs.ChangeScriptConfig{}
restart := false
var splay time.Duration

Expand Down Expand Up @@ -436,6 +440,8 @@ func (tm *TaskTemplateManager) onTemplateRendered(handledRenders map[string]time
signals[tmpl.ChangeSignal] = struct{}{}
case structs.TemplateChangeModeRestart:
restart = true
case structs.TemplateChangeModeScript:
scripts = append(scripts, tmpl.ChangeScriptConfig)
case structs.TemplateChangeModeNoop:
continue
}
Expand Down Expand Up @@ -493,6 +499,32 @@ func (tm *TaskTemplateManager) onTemplateRendered(handledRenders map[string]time
}
}
}
if len(scripts) != 0 {
pkazmierczak marked this conversation as resolved.
Show resolved Hide resolved
for _, script := range scripts {
_, exitCode, err := tm.config.Handle.Exec(script.Timeout, script.Path, script.Args)
if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker, but we should ask @lgfa29 if this should introduce a new metric or increment an existing one.

structs.NewTaskEvent(structs.TaskHookFailed).
SetDisplayMessage(
fmt.Sprintf(
"Template failed to run script %v on change: %v Exit code: %v", script.Path, err, exitCode,
))
if script.FailOnError {
tm.config.Lifecycle.Kill(context.Background(),
structs.NewTaskEvent(structs.TaskKilling).
SetFailsTask().
SetDisplayMessage(
fmt.Sprintf("Template failed to run script %v and the task is being killed", script.Path),
))
}
} else {
tm.config.Events.EmitEvent(structs.NewTaskEvent(structs.TaskHookMessage).
SetDisplayMessage(
fmt.Sprintf(
"Template successfully ran a script from %v with exit code: %v", script.Path, exitCode,
)))
}
}
}

}

Expand Down
175 changes: 175 additions & 0 deletions client/allocrunner/taskrunner/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
ctestutil "github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/allocrunner/taskrunner/interfaces"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/taskenv"
"github.com/hashicorp/nomad/helper"
Expand Down Expand Up @@ -123,6 +124,16 @@ func (m *MockTaskHooks) EmitEvent(event *structs.TaskEvent) {

func (m *MockTaskHooks) SetState(state string, event *structs.TaskEvent) {}

// mockExecutor implements script executor interface
type mockExecutor struct {
pkazmierczak marked this conversation as resolved.
Show resolved Hide resolved
DesiredExit int
DesiredErr error
}

func (m *mockExecutor) Exec(timeout time.Duration, cmd string, args []string) ([]byte, int, error) {
return []byte{}, m.DesiredExit, m.DesiredErr
}

// testHarness is used to test the TaskTemplateManager by spinning up
// Consul/Vault as needed
type testHarness struct {
Expand All @@ -138,6 +149,7 @@ type testHarness struct {
consul *ctestutil.TestServer
emitRate time.Duration
nomadNamespace string
driver interfaces.ScriptExecutor
}

// newTestHarness returns a harness starting a dev consul and vault server,
Expand Down Expand Up @@ -211,6 +223,7 @@ func (h *testHarness) startWithErr() error {
VaultToken: h.vaultToken,
TaskDir: h.taskDir,
EnvBuilder: h.envBuilder,
Handle: h.driver,
MaxTemplateEventRate: h.emitRate,
})

Expand Down Expand Up @@ -1201,6 +1214,168 @@ func TestTaskTemplateManager_Signal_Error(t *testing.T) {
require.Contains(harness.mockHooks.KillEvent.DisplayMessage, "failed to send signals")
}

func TestTaskTemplateManager_ScriptExecution(t *testing.T) {
ci.Parallel(t)

// Make a template that renders based on a key in Consul and triggers script
key1 := "bam"
key2 := "bar"
content1_1 := "cat"
content1_2 := "dog"
t1 := &structs.Template{
EmbeddedTmpl: `
FOO={{key "bam"}}
`,
DestPath: "test.env",
ChangeMode: structs.TemplateChangeModeScript,
ChangeScriptConfig: &structs.ChangeScriptConfig{
Path: "/bin/foo",
Args: []string{},
Timeout: 5 * time.Second,
FailOnError: false,
},
Envvars: true,
}
t2 := &structs.Template{
EmbeddedTmpl: `
BAR={{key "bar"}}
`,
DestPath: "test2.env",
ChangeMode: structs.TemplateChangeModeScript,
ChangeScriptConfig: &structs.ChangeScriptConfig{
Path: "/bin/foo",
Args: []string{},
Timeout: 5 * time.Second,
FailOnError: false,
},
Envvars: true,
}

me := mockExecutor{}
harness := newTestHarness(t, []*structs.Template{t1, t2}, true, false)
harness.driver = &me
harness.start(t)
defer harness.stop()

// Ensure no unblock
select {
case <-harness.mockHooks.UnblockCh:
require.Fail(t, "Task unblock should not have been called")
case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second):
}

// Write the key to Consul
harness.consul.SetKV(t, key1, []byte(content1_1))
harness.consul.SetKV(t, key2, []byte(content1_1))

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

// Update the keys in Consul
harness.consul.SetKV(t, key1, []byte(content1_2))

// Wait for restart
timeout := time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second)
OUTER:
for {
select {
case <-harness.mockHooks.RestartCh:
require.Fail(t, "restart not expected")
case ev := <-harness.mockHooks.EmitEventCh:
if strings.Contains(ev.DisplayMessage, t1.ChangeScriptConfig.Path) {
break OUTER
}
case <-harness.mockHooks.SignalCh:
require.Fail(t, "signal not expected")
case <-timeout:
require.Fail(t, "should have received an 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) {
ci.Parallel(t)
require := require.New(t)

// Make a template that renders based on a key in Consul and triggers script
key1 := "bam"
key2 := "bar"
content1_1 := "cat"
content1_2 := "dog"
t1 := &structs.Template{
EmbeddedTmpl: `
FOO={{key "bam"}}
`,
DestPath: "test.env",
ChangeMode: structs.TemplateChangeModeScript,
ChangeScriptConfig: &structs.ChangeScriptConfig{
Path: "/bin/foo",
Args: []string{},
Timeout: 5 * time.Second,
FailOnError: true,
},
Envvars: true,
}
t2 := &structs.Template{
EmbeddedTmpl: `
BAR={{key "bar"}}
`,
DestPath: "test2.env",
ChangeMode: structs.TemplateChangeModeScript,
ChangeScriptConfig: &structs.ChangeScriptConfig{
Path: "/bin/foo",
Args: []string{},
Timeout: 5 * time.Second,
FailOnError: false,
},
Envvars: true,
}

me := mockExecutor{DesiredExit: 1, DesiredErr: fmt.Errorf("Script failed")}
harness := newTestHarness(t, []*structs.Template{t1, t2}, true, false)
harness.driver = &me
harness.start(t)
defer harness.stop()

// Ensure no unblock
select {
case <-harness.mockHooks.UnblockCh:
require.Fail("Task unblock should not have been called")
case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second):
}

// Write the key to Consul
harness.consul.SetKV(t, key1, []byte(content1_1))
harness.consul.SetKV(t, key2, []byte(content1_1))

// Wait for the unblock
select {
case <-harness.mockHooks.UnblockCh:
case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second):
require.Fail("Task unblock should have been called")
}

// Update the keys in Consul
harness.consul.SetKV(t, key1, []byte(content1_2))

// Wait for kill channel
select {
case <-harness.mockHooks.KillCh:
break
case <-time.After(time.Duration(1*testutil.TestMultiplier()) * time.Second):
require.Fail("Should have received a signals: %+v", harness.mockHooks)
}

require.NotNil(harness.mockHooks.KillEvent)
require.Contains(harness.mockHooks.KillEvent.DisplayMessage, "failed to run script")
}

// TestTaskTemplateManager_FiltersProcessEnvVars asserts that we only render
// environment variables found in task env-vars and not read the nomad host
// process environment variables. nomad host process environment variables
Expand Down
4 changes: 4 additions & 0 deletions client/allocrunner/taskrunner/template_hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ type templateHookConfig struct {

// nomadNamespace is the job's Nomad namespace
nomadNamespace string

// handle is the driver handle that allows driver operations
handle *DriverHandle
}

type templateHook struct {
Expand Down Expand Up @@ -131,6 +134,7 @@ func (h *templateHook) newManager() (unblock chan struct{}, err error) {
MaxTemplateEventRate: template.DefaultMaxTemplateEventRate,
NomadNamespace: h.config.nomadNamespace,
NomadToken: h.nomadToken,
Handle: h.config.handle,
})
if err != nil {
h.logger.Error("failed to create template manager", "error", err)
Expand Down
Loading