Skip to content

Commit b0749c7

Browse files
authored
Merge pull request #1839 from hashicorp/f-signal-constraints
Signal creates an auto-constraints
2 parents c7889f2 + b580a6a commit b0749c7

24 files changed

+623
-22
lines changed

client/driver/docker.go

+6
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,12 @@ func (d *DockerDriver) Validate(config map[string]interface{}) error {
267267
return nil
268268
}
269269

270+
func (d *DockerDriver) Abilities() DriverAbilities {
271+
return DriverAbilities{
272+
SendSignals: true,
273+
}
274+
}
275+
270276
// dockerClients creates two *docker.Client, one for long running operations and
271277
// the other for shorter operations. In test / dev mode we can use ENV vars to
272278
// connect to the docker daemon. In production mode we will read docker.endpoint

client/driver/driver.go

+9
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ type Driver interface {
5959

6060
// Drivers must validate their configuration
6161
Validate(map[string]interface{}) error
62+
63+
// Abilities returns the abilities of the driver
64+
Abilities() DriverAbilities
65+
}
66+
67+
// DriverAbilities marks the abilities the driver has.
68+
type DriverAbilities struct {
69+
// SendSignals marks the driver as being able to send signals
70+
SendSignals bool
6271
}
6372

6473
// DriverContext is a means to inject dependencies such as loggers, configs, and

client/driver/exec.go

+6
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ func (d *ExecDriver) Validate(config map[string]interface{}) error {
8282
return nil
8383
}
8484

85+
func (d *ExecDriver) Abilities() DriverAbilities {
86+
return DriverAbilities{
87+
SendSignals: true,
88+
}
89+
}
90+
8591
func (d *ExecDriver) Periodic() (bool, time.Duration) {
8692
return true, 15 * time.Second
8793
}

client/driver/java.go

+6
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ func (d *JavaDriver) Validate(config map[string]interface{}) error {
9494
return nil
9595
}
9696

97+
func (d *JavaDriver) Abilities() DriverAbilities {
98+
return DriverAbilities{
99+
SendSignals: true,
100+
}
101+
}
102+
97103
func (d *JavaDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
98104
// Get the current status so that we can log any debug messages only if the
99105
// state changes

client/driver/mock_driver.go

+6
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ func NewMockDriver(ctx *DriverContext) Driver {
6262
return &MockDriver{DriverContext: *ctx}
6363
}
6464

65+
func (d *MockDriver) Abilities() DriverAbilities {
66+
return DriverAbilities{
67+
SendSignals: false,
68+
}
69+
}
70+
6571
// Start starts the mock driver
6672
func (m *MockDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) {
6773
var driverConfig MockDriverConfig

client/driver/qemu.go

+6
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ func (d *QemuDriver) Validate(config map[string]interface{}) error {
9797
return nil
9898
}
9999

100+
func (d *QemuDriver) Abilities() DriverAbilities {
101+
return DriverAbilities{
102+
SendSignals: false,
103+
}
104+
}
105+
100106
func (d *QemuDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
101107
// Get the current status so that we can log any debug messages only if the
102108
// state changes

client/driver/raw_exec.go

+6
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ func (d *RawExecDriver) Validate(config map[string]interface{}) error {
8181
return nil
8282
}
8383

84+
func (d *RawExecDriver) Abilities() DriverAbilities {
85+
return DriverAbilities{
86+
SendSignals: true,
87+
}
88+
}
89+
8490
func (d *RawExecDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
8591
// Get the current status so that we can log any debug messages only if the
8692
// state changes

client/driver/rkt.go

+6
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ func (d *RktDriver) Validate(config map[string]interface{}) error {
135135
return nil
136136
}
137137

138+
func (d *RktDriver) Abilities() DriverAbilities {
139+
return DriverAbilities{
140+
SendSignals: false,
141+
}
142+
}
143+
138144
func (d *RktDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
139145
// Get the current status so that we can log any debug messages only if the
140146
// state changes

client/fingerprint/fingerprint.go

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func init() {
2525
builtinFingerprintMap["memory"] = NewMemoryFingerprint
2626
builtinFingerprintMap["network"] = NewNetworkFingerprint
2727
builtinFingerprintMap["nomad"] = NewNomadFingerprint
28+
builtinFingerprintMap["signal"] = NewSignalFingerprint
2829
builtinFingerprintMap["storage"] = NewStorageFingerprint
2930
builtinFingerprintMap["vault"] = NewVaultFingerprint
3031

client/fingerprint/signal.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package fingerprint
2+
3+
import (
4+
"log"
5+
"strings"
6+
7+
"github.com/hashicorp/consul-template/signals"
8+
"github.com/hashicorp/nomad/client/config"
9+
"github.com/hashicorp/nomad/nomad/structs"
10+
)
11+
12+
// SignalFingerprint is used to fingerprint the available signals
13+
type SignalFingerprint struct {
14+
StaticFingerprinter
15+
logger *log.Logger
16+
}
17+
18+
// NewSignalFingerprint is used to create a Signal fingerprint
19+
func NewSignalFingerprint(logger *log.Logger) Fingerprint {
20+
f := &SignalFingerprint{logger: logger}
21+
return f
22+
}
23+
24+
func (f *SignalFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
25+
// Build the list of available signals
26+
sigs := make([]string, 0, len(signals.SignalLookup))
27+
for signal := range signals.SignalLookup {
28+
sigs = append(sigs, signal)
29+
}
30+
31+
node.Attributes["os.signals"] = strings.Join(sigs, ",")
32+
return true, nil
33+
}

client/fingerprint/signal_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package fingerprint
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/nomad/nomad/structs"
7+
)
8+
9+
func TestSignalFingerprint(t *testing.T) {
10+
fp := NewSignalFingerprint(testLogger())
11+
node := &structs.Node{
12+
Attributes: make(map[string]string),
13+
}
14+
15+
assertFingerprintOK(t, fp, node)
16+
assertNodeAttributeContains(t, node, "os.signals")
17+
}

jobspec/parse.go

+8
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ func parseConstraints(result *[]*structs.Constraint, list *ast.ObjectList) error
407407
"version",
408408
"regexp",
409409
"distinct_hosts",
410+
"set_contains",
410411
}
411412
if err := checkHCLKeys(o.Val, valid); err != nil {
412413
return err
@@ -435,6 +436,13 @@ func parseConstraints(result *[]*structs.Constraint, list *ast.ObjectList) error
435436
m["RTarget"] = constraint
436437
}
437438

439+
// If "set_contains" is provided, set the operand
440+
// to "set_contains" and the value to the "RTarget"
441+
if constraint, ok := m[structs.ConstraintSetContains]; ok {
442+
m["Operand"] = structs.ConstraintSetContains
443+
m["RTarget"] = constraint
444+
}
445+
438446
if value, ok := m[structs.ConstraintDistinctHosts]; ok {
439447
enabled, err := parseBool(value)
440448
if err != nil {

jobspec/parse_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,25 @@ func TestParse(t *testing.T) {
282282
false,
283283
},
284284

285+
{
286+
"set-contains-constraint.hcl",
287+
&structs.Job{
288+
ID: "foo",
289+
Name: "foo",
290+
Priority: 50,
291+
Region: "global",
292+
Type: "service",
293+
Constraints: []*structs.Constraint{
294+
&structs.Constraint{
295+
LTarget: "$meta.data",
296+
RTarget: "foo,bar,baz",
297+
Operand: structs.ConstraintSetContains,
298+
},
299+
},
300+
},
301+
false,
302+
},
303+
285304
{
286305
"distinctHosts-constraint.hcl",
287306
&structs.Job{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
job "foo" {
2+
constraint {
3+
attribute = "$meta.data"
4+
set_contains = "foo,bar,baz"
5+
}
6+
}

nomad/job_endpoint.go

+98-22
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegis
5252
// Initialize the job fields (sets defaults and any necessary init work).
5353
args.Job.Canonicalize()
5454

55+
// Add implicit constraints
56+
setImplicitConstraints(args.Job)
57+
5558
// Validate the job.
5659
if err := validateJob(args.Job); err != nil {
5760
return err
@@ -115,28 +118,6 @@ func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegis
115118
}
116119
}
117120
}
118-
119-
// Add implicit constraints that the task groups are run on a Node with
120-
// Vault
121-
for _, tg := range args.Job.TaskGroups {
122-
_, ok := policies[tg.Name]
123-
if !ok {
124-
// Not requesting Vault
125-
continue
126-
}
127-
128-
found := false
129-
for _, c := range tg.Constraints {
130-
if c.Equal(vaultConstraint) {
131-
found = true
132-
break
133-
}
134-
}
135-
136-
if !found {
137-
tg.Constraints = append(tg.Constraints, vaultConstraint)
138-
}
139-
}
140121
}
141122

142123
// Clear the Vault token
@@ -188,6 +169,77 @@ func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegis
188169
return nil
189170
}
190171

172+
// setImplicitConstraints adds implicit constraints to the job based on the
173+
// features it is requesting.
174+
func setImplicitConstraints(j *structs.Job) {
175+
// Get the required Vault Policies
176+
policies := j.VaultPolicies()
177+
178+
// Get the required signals
179+
signals := j.RequiredSignals()
180+
181+
// Hot path
182+
if len(signals) == 0 && len(policies) == 0 {
183+
return
184+
}
185+
186+
// Add Vault constraints
187+
for _, tg := range j.TaskGroups {
188+
_, ok := policies[tg.Name]
189+
if !ok {
190+
// Not requesting Vault
191+
continue
192+
}
193+
194+
found := false
195+
for _, c := range tg.Constraints {
196+
if c.Equal(vaultConstraint) {
197+
found = true
198+
break
199+
}
200+
}
201+
202+
if !found {
203+
tg.Constraints = append(tg.Constraints, vaultConstraint)
204+
}
205+
}
206+
207+
// Add signal constraints
208+
for _, tg := range j.TaskGroups {
209+
tgSignals, ok := signals[tg.Name]
210+
if !ok {
211+
// Not requesting Vault
212+
continue
213+
}
214+
215+
// Flatten the signals
216+
required := structs.MapStringStringSliceValueSet(tgSignals)
217+
sigConstraint := getSignalConstraint(required)
218+
219+
found := false
220+
for _, c := range tg.Constraints {
221+
if c.Equal(sigConstraint) {
222+
found = true
223+
break
224+
}
225+
}
226+
227+
if !found {
228+
tg.Constraints = append(tg.Constraints, sigConstraint)
229+
}
230+
}
231+
}
232+
233+
// getSignalConstraint builds a suitable constraint based on the required
234+
// signals
235+
func getSignalConstraint(signals []string) *structs.Constraint {
236+
return &structs.Constraint{
237+
Operand: structs.ConstraintSetContains,
238+
LTarget: "${attr.os.signals}",
239+
RTarget: strings.Join(signals, ","),
240+
}
241+
}
242+
191243
// Summary retreives the summary of a job
192244
func (j *Job) Summary(args *structs.JobSummaryRequest,
193245
reply *structs.JobSummaryResponse) error {
@@ -556,6 +608,9 @@ func (j *Job) Plan(args *structs.JobPlanRequest, reply *structs.JobPlanResponse)
556608
// Initialize the job fields (sets defaults and any necessary init work).
557609
args.Job.Canonicalize()
558610

611+
// Add implicit constraints
612+
setImplicitConstraints(args.Job)
613+
559614
// Validate the job.
560615
if err := validateJob(args.Job); err != nil {
561616
return err
@@ -656,8 +711,14 @@ func validateJob(job *structs.Job) error {
656711
multierror.Append(validationErrors, err)
657712
}
658713

714+
// Get the signals required
715+
signals := job.RequiredSignals()
716+
659717
// Validate the driver configurations.
660718
for _, tg := range job.TaskGroups {
719+
// Get the signals for the task group
720+
tgSignals, tgOk := signals[tg.Name]
721+
661722
for _, task := range tg.Tasks {
662723
d, err := driver.NewDriver(
663724
task.Driver,
@@ -673,6 +734,21 @@ func validateJob(job *structs.Job) error {
673734
formatted := fmt.Errorf("group %q -> task %q -> config: %v", tg.Name, task.Name, err)
674735
multierror.Append(validationErrors, formatted)
675736
}
737+
738+
// The task group didn't have any task that required signals
739+
if !tgOk {
740+
continue
741+
}
742+
743+
// This task requires signals. Ensure the driver is capable
744+
if required, ok := tgSignals[task.Name]; ok {
745+
abilities := d.Abilities()
746+
if !abilities.SendSignals {
747+
formatted := fmt.Errorf("group %q -> task %q: driver %q doesn't support sending signals. Requested signals are %v",
748+
tg.Name, task.Name, task.Driver, strings.Join(required, ", "))
749+
multierror.Append(validationErrors, formatted)
750+
}
751+
}
676752
}
677753
}
678754

0 commit comments

Comments
 (0)