Skip to content
This repository has been archived by the owner on Jan 30, 2020. It is now read-only.

feat(job): substitute Requirements for instance unit jobs #479

Merged
merged 1 commit into from
May 29, 2014
Merged
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
15 changes: 15 additions & 0 deletions Documentation/scheduling.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,18 @@ Follower units will reschedule themselves around the cluster to ensure their `X-
The value of the `X-Conflicts` option is a [glob pattern](http://golang.org/pkg/path/#Match) defining which other units next to which a given unit must not be scheduled.

If a unit is scheduled to the system without an `X-Conflicts` option, other units' conflicts still take effect and prevent the new unit from being scheduled to machines where conflicts exist.

##### Dynamic requirements

fleet supports several systemd specifiers to allow requirements to be dynamically determined based on a Job's name. This means that the same unit can be used for multiple Jobs and the requirements are dynamically substituted when the Job is scheduled.

For example, a Job by the name `foo.service`, whose unit contains the following snippet:

```
[X-Fleet]
X-ConditionMachineOf=%p.socket
```

would result in an effective `X-ConditionMachineOf` of `foo.socket`. Using the same unit snippet with a Job called `bar.service`, on the other hand, would result in an effective `X-ConditionMachineOf` of `bar.socket`.

For more information on the available specifiers, see the [unit file configuration](Documentation/unit-files.md#systemd-specifiers) documentation.
13 changes: 12 additions & 1 deletion Documentation/unit-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,15 @@ ExecStart=/bin/monitorme
X-ConditionMachineID=148a18ff-6e95-4cd8-92da-c9de9bb90d5a
```

Note that [Systemd Specifiers](http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers) are not yet available within `[X-Fleet]` sections. See [the issue](https://github.com/coreos/fleet/issues/303) for more information.
## systemd specifiers

When evaluating the `[X-Fleet]` section, fleet supports a subset of systemd's [specifiers][systemd specifiers] to perform variable substitution. The following specifiers are currently supported:

* `%n`
* `%N`
* `%p`
* `%i`

For the meaning of the specifiers, refer to the official [systemd documentation][systemd specifiers].

[systemd specifiers]: http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers
27 changes: 25 additions & 2 deletions job/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ func (j *Job) IsBatch() bool {
// Requirements returns all relevant options from the [X-Fleet] section of a unit file.
// Relevant options are identified with a `X-` prefix in the unit.
// This prefix is stripped from relevant options before being returned.
// Furthermore, specifier substitution (using unitPrintf) is performed on all requirements.
func (j *Job) Requirements() map[string][]string {
uni := unit.NewUnitNameInfo(j.Name)
requirements := make(map[string][]string)
for key, value := range j.Unit.Contents["X-Fleet"] {
for key, values := range j.Unit.Contents["X-Fleet"] {
if !strings.HasPrefix(key, "X-") {
continue
}
Expand All @@ -103,7 +105,13 @@ func (j *Job) Requirements() map[string][]string {
requirements[key] = make([]string, 0)
}

requirements[key] = value
if uni != nil {
for i, v := range values {
values[i] = unitPrintf(v, *uni)
}
}

requirements[key] = values
}

return requirements
Expand Down Expand Up @@ -210,3 +218,18 @@ func (j *Job) RequiredTargetMetadata() map[string][]string {

return metadata
}

// unitPrintf is analogous to systemd's `unit_name_printf`. It will take the
// given string and replace the following specifiers with the values from the
// provided UnitNameInfo:
// %n: the full name of the unit ([email protected])
// %N: the name of the unit without the suffix (foo@bar)
// %p: the prefix (foo)
// %i: the instance (bar)
func unitPrintf(s string, nu unit.UnitNameInfo) (out string) {
out = strings.Replace(s, "%n", nu.FullName, -1)
out = strings.Replace(out, "%N", nu.Name, -1)
out = strings.Replace(out, "%p", nu.Prefix, -1)
out = strings.Replace(out, "%i", nu.Instance, -1)
return
}
63 changes: 63 additions & 0 deletions job/job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,49 @@ X-Ping=Pang
}
}

func TestParseRequirementsInstanceUnit(t *testing.T) {
contents := `
[X-Fleet]
X-Foo=%n
X-Bar=%N
X-Baz=%p
X-Qux=%i
X-Zzz=something
`
// Ensure the correct values are replaced for a non-instance unit
j := NewJob("test.service", *unit.NewUnit(contents))
reqs := j.Requirements()
for field, want := range map[string]string{
"Foo": "test.service",
"Bar": "test",
"Baz": "test",
"Qux": "",
"Zzz": "something",
} {
got := reqs[field]
if len(got) != 1 || got[0] != want {
t.Errorf("Requirement %q unexpectedly altered for non-instance unit: want %q, got %q", field, want, got)
}
}

// Now ensure that they are substituted appropriately for an instance unit
j = NewJob("[email protected]", *unit.NewUnit(contents))
reqs = j.Requirements()
for field, want := range map[string]string{
"Foo": "[email protected]",
"Bar": "ssh@2",
"Baz": "ssh",
"Qux": "2",
"Zzz": "something",
} {
got := reqs[field]
if len(got) != 1 || got[0] != want {
t.Errorf("Bad instance unit requirement substitution for %q: want %q, got %q", field, want, got)
}
}

}

func TestParseRequirementsMissingSection(t *testing.T) {
contents := `
[Unit]
Expand Down Expand Up @@ -245,5 +288,25 @@ Type=oneshot`))
if !bj.IsBatch() {
t.Error("IsBatch() on oneshot job returned false unexpectedly")
}
}

func TestInstanceUnitPrintf(t *testing.T) {
u := unit.NewUnitNameInfo("[email protected]")
if u == nil {
t.Fatal("NewNamedUnit returned nil - aborting")
}
for _, tt := range []struct {
in string
want string
}{
{"%n", "[email protected]"},
{"%N", "foo@bar"},
{"%p", "foo"},
{"%i", "bar"},
} {
got := unitPrintf(tt.in, *u)
if got != tt.want {
t.Errorf("Replacement of %q failed: got %q, want %q", tt.in, got, tt.want)
}
}
}
56 changes: 37 additions & 19 deletions unit/unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,32 +79,50 @@ func NewUnitState(loadState, activeState, subState string, ms *machine.MachineSt
return &UnitState{loadState, activeState, subState, ms}
}

// InstanceUnit represents a Unit that has been instantiated from a template unit
type InstanceUnit struct {
FullName string // Original name of the template unit (e.g. [email protected])
Template string // Name of the canonical template unit (e.g. [email protected])
// UnitNameInfo exposes certain interesting items about a Unit based on its
// name. For example, a unit with the name "[email protected]" constitutes a
// template unit, and a unit named "[email protected]" would represent an instance
// unit of that template.
type UnitNameInfo struct {
FullName string // Original complete name of the unit (e.g. foo.socket, [email protected])
Name string // Name of the unit without suffix (e.g. foo, foo@bar)
Prefix string // Prefix of the template unit (e.g. foo)

// If the unit represents an instance or a template, the following values are set
Template string // Name of the canonical template unit (e.g. [email protected])
Instance string // Instance name (e.g. bar)
}

// UnitNameToInstance determines whether the given unit name appears to be an instance
// of a template unit. If so, it returns a non-nil *InstanceUnit; otherwise, nil.
func UnitNameToInstance(name string) *InstanceUnit {
// IsInstance returns a boolean indicating whether the UnitNameInfo appears to be
// an Instance of a Template unit
func (nu UnitNameInfo) IsInstance() bool {
return len(nu.Instance) > 0
}

// NewUnitNameInfo generates a UnitNameInfo from the given name. If the given string
// is not a correct unit name, nil is returned.
func NewUnitNameInfo(un string) *UnitNameInfo {

// Everything past the first @ and before the last . is the instance
s := strings.LastIndex(name, ".")
s := strings.LastIndex(un, ".")
if s == -1 {
return nil
}
suffix := name[s:]
prefix := name[:s]
if !strings.Contains(prefix, "@") {
return nil
}
a := strings.Index(prefix, "@")
return &InstanceUnit{
FullName: name,
Template: fmt.Sprintf("%s@%s", prefix[:a], suffix),
Prefix: prefix[:a],
Instance: prefix[a+1:],

nu := &UnitNameInfo{FullName: un}
name := un[:s]
suffix := un[s:]
nu.Name = name

a := strings.Index(name, "@")
if a == -1 {
// This does not appear to be a template or instance unit.
nu.Prefix = name
return nu
}

nu.Prefix = name[:a]
nu.Template = fmt.Sprintf("%s@%s", name[:a], suffix)
nu.Instance = name[a+1:]
return nu
}
55 changes: 32 additions & 23 deletions unit/unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,46 +96,55 @@ func TestNewUnitState(t *testing.T) {

}

func TestInstanceUnit(t *testing.T) {
func TestNamedUnit(t *testing.T) {
tts := []struct {
name string
tmpl string
pref string
inst string
fname string
name string
pref string
tmpl string
inst string
isinst bool
}{
// Everything past the first @ and before the last . is the instance
{"[email protected]", "foo@.service", "foo", ""},
{"[email protected]", "foo@.service", "foo", "bar"},
{"foo@[email protected]", "foo@.service", "foo", "bar@baz"},
{"[email protected]", "foo.1@.service", "foo.1", ""},
{"[email protected]", "foo.1@.service", "foo.1", "2"},
{"[email protected]", "ssh@.socket", "ssh", ""},
{"[email protected]", "ssh@.socket", "ssh", "1"},
{"foo.service", "foo", "foo", "", "", false},
{"[email protected]", "foo@", "foo", "[email protected]", "", false},
{"[email protected]", "foo@bar", "foo", "[email protected]", "bar", true},
{"foo@[email protected]", "foo@bar@baz", "foo", "[email protected]", "bar@baz", true},
{"[email protected]", "foo.1@", "foo.1", "foo.1@.service", "", false},
{"[email protected]", "foo.1@2", "foo.1", "foo.1@.service", "2", true},
{"[email protected]", "ssh@", "ssh", "[email protected]", "", false},
{"[email protected]", "ssh@1", "ssh", "[email protected]", "1", true},
}
for _, tt := range tts {
u := UnitNameToInstance(tt.name)
u := NewUnitNameInfo(tt.fname)
if u == nil {
t.Errorf("UnitNameToInstance(%s) returned nil InstanceUnit!", tt.name)
t.Errorf("NewUnitNameInfo(%s) returned nil InstanceUnit!", tt.name)
continue
}
if u.FullName != tt.name {
t.Errorf("UnitNameToInstance(%s) returned bad name: got %s, want %s", tt.name, u.FullName, tt.name)
if u.FullName != tt.fname {
t.Errorf("NewUnitNameInfo(%s) returned bad fullname: got %s, want %s", tt.name, u.FullName, tt.fname)
}
if u.Name != tt.name {
t.Errorf("NewUnitNameInfo(%s) returned bad name: got %s, want %s", tt.name, u.Name, tt.name)
}
if u.Template != tt.tmpl {
t.Errorf("UnitNameToInstance(%s) returned bad template name: got %s, want %s", tt.name, u.Template, tt.tmpl)
t.Errorf("NewUnitNameInfo(%s) returned bad template name: got %s, want %s", tt.name, u.Template, tt.tmpl)
}
if u.Prefix != tt.pref {
t.Errorf("UnitNameToInstance(%s) returned bad prefix name: got %s, want %s", tt.name, u.Prefix, tt.pref)
t.Errorf("NewUnitNameInfo(%s) returned bad prefix name: got %s, want %s", tt.name, u.Prefix, tt.pref)
}
if u.Instance != tt.inst {
t.Errorf("UnitNameToInstance(%s) returned bad instance name: got %s, want %s", tt.name, u.Instance, tt.inst)
t.Errorf("NewUnitNameInfo(%s) returned bad instance name: got %s, want %s", tt.name, u.Instance, tt.inst)
}
i := u.IsInstance()
if i != tt.isinst {
t.Errorf("NewUnitNameInfo(%s).IsInstance returned %t, want %s", tt.name, i, tt.isinst)
}
}

bad := []string{"foo.service", "foo@", "bar.socket", "ssh.1.service"}
bad := []string{"foo", "bar@baz"}
for _, tt := range bad {
if UnitNameToInstance(tt) != nil {
t.Errorf("UnitNameToInstance returned non-nil InstanceUnit unexpectedly")
if NewUnitNameInfo(tt) != nil {
t.Errorf("NewUnitNameInfo returned non-nil InstanceUnit unexpectedly")
}
}

Expand Down