drop-in: add support for drop-ins

This allows a list of drop-ins for a unit to be declared inline within a
cloud-config. For example:

  #cloud-config
  coreos:
    units:
      - name: docker.service
        drop-ins:
          - name: 50-insecure-registry.conf
            content: |
              [Service]
              Environment=DOCKER_OPTS='--insecure-registry="10.0.1.0/24"'
This commit is contained in:
Alex Crawford 2014-11-25 16:57:15 -08:00
parent 420f7cf202
commit ffc54b028c
17 changed files with 329 additions and 105 deletions

View File

@ -16,7 +16,7 @@ We've designed our implementation to allow the same cloud-config file to work ac
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values. The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
A cloud-config file should contain `#cloud-config`, followed by an associative array which has zero or more of the following keys: A cloud-config file must contain `#cloud-config`, followed by an associative array which has zero or more of the following keys:
- `coreos` - `coreos`
- `ssh_authorized_keys` - `ssh_authorized_keys`
@ -66,7 +66,6 @@ Environment="ETCD_PEER_ADDR=192.0.2.13:7001"
``` ```
For more information about the available configuration parameters, see the [etcd documentation][etcd-config]. For more information about the available configuration parameters, see the [etcd documentation][etcd-config].
Note that hyphens in the coreos.etcd.* keys are mapped to underscores.
_Note: The `$private_ipv4` and `$public_ipv4` substitution variables referenced in other documents are only supported on Amazon EC2, Google Compute Engine, OpenStack, Rackspace, DigitalOcean, and Vagrant._ _Note: The `$private_ipv4` and `$public_ipv4` substitution variables referenced in other documents are only supported on Amazon EC2, Google Compute Engine, OpenStack, Rackspace, DigitalOcean, and Vagrant._
@ -158,6 +157,10 @@ Each item is an object with the following fields:
- **content**: Plaintext string representing entire unit file. If no value is provided, the unit is assumed to exist already. - **content**: Plaintext string representing entire unit file. If no value is provided, the unit is assumed to exist already.
- **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. The default behavior is to not execute any commands. - **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. The default behavior is to not execute any commands.
- **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. The default value is false. - **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. The default value is false.
- **drop-ins**: A list of unit drop-ins with the following fields:
- **name**: String representing unit's name. Required.
- **content**: Plaintext string representing entire file. Required.
**NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place. **NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place.
@ -184,6 +187,21 @@ coreos:
ExecStop=/usr/bin/docker stop -t 2 redis_server ExecStop=/usr/bin/docker stop -t 2 redis_server
``` ```
Add the DOCKER_OPTS environment variable to docker.service.
```yaml
#cloud-config
coreos:
units:
- name: docker.service
drop-ins:
- name: 50-insecure-registry.conf
content: |
[Service]
Environment=DOCKER_OPTS='--insecure-registry="10.0.1.0/24"'
```
Start the built-in `etcd` and `fleet` services: Start the built-in `etcd` and `fleet` services:
```yaml ```yaml

View File

@ -335,26 +335,6 @@ func TestCloudConfigSerializationHeader(t *testing.T) {
} }
} }
// TestDropInIgnored asserts that users are unable to set DropIn=True on units
func TestDropInIgnored(t *testing.T) {
contents := `
coreos:
units:
- name: test
dropin: true
`
cfg, err := NewCloudConfig(contents)
if err != nil || len(cfg.Coreos.Units) != 1 {
t.Fatalf("Encountered unexpected error: %v", err)
}
if len(cfg.Coreos.Units) != 1 || cfg.Coreos.Units[0].Name != "test" {
t.Fatalf("Expected 1 unit, but got %d: %v", len(cfg.Coreos.Units), cfg.Coreos.Units)
}
if cfg.Coreos.Units[0].DropIn {
t.Errorf("dropin option on unit in cloud-config was not ignored!")
}
}
func TestCloudConfigUsers(t *testing.T) { func TestCloudConfigUsers(t *testing.T) {
contents := ` contents := `
users: users:

View File

@ -23,9 +23,10 @@ type Unit struct {
Runtime bool `yaml:"runtime"` Runtime bool `yaml:"runtime"`
Content string `yaml:"content"` Content string `yaml:"content"`
Command string `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"` Command string `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"`
DropIns []UnitDropIn `yaml:"drop_ins"`
// For drop-in units, a cloudinit.conf is generated. }
// This is currently unbound in YAML (and hence unsettable in cloud-config files)
// until the correct behaviour for multiple drop-in units is determined. type UnitDropIn struct {
DropIn bool `yaml:"-"` Name string `yaml:"name"`
Content string `yaml:"content"`
} }

View File

@ -201,6 +201,11 @@ func processUnits(units []system.Unit, root string, um system.UnitManager) error
actions := make([]action, 0, len(units)) actions := make([]action, 0, len(units))
reload := false reload := false
for _, unit := range units { for _, unit := range units {
if unit.Name == "" {
log.Printf("Skipping unit without name")
continue
}
if unit.Content != "" { if unit.Content != "" {
log.Printf("Writing unit %q to filesystem", unit.Name) log.Printf("Writing unit %q to filesystem", unit.Name)
if err := um.PlaceUnit(unit); err != nil { if err := um.PlaceUnit(unit); err != nil {
@ -210,6 +215,17 @@ func processUnits(units []system.Unit, root string, um system.UnitManager) error
reload = true reload = true
} }
for _, dropin := range unit.DropIns {
if dropin.Name != "" && dropin.Content != "" {
log.Printf("Writing drop-in unit %q to filesystem", dropin.Name)
if err := um.PlaceUnitDropIn(unit, dropin); err != nil {
return err
}
log.Printf("Wrote drop-in unit %q", dropin.Name)
reload = true
}
}
if unit.Mask { if unit.Mask {
log.Printf("Masking unit file %q", unit.Name) log.Printf("Masking unit file %q", unit.Name)
if err := um.MaskUnit(unit); err != nil { if err := um.MaskUnit(unit); err != nil {

View File

@ -37,6 +37,10 @@ func (tum *TestUnitManager) PlaceUnit(u system.Unit) error {
tum.placed = append(tum.placed, u.Name) tum.placed = append(tum.placed, u.Name)
return nil return nil
} }
func (tum *TestUnitManager) PlaceUnitDropIn(u system.Unit, d config.UnitDropIn) error {
tum.placed = append(tum.placed, u.Name+".d/"+d.Name)
return nil
}
func (tum *TestUnitManager) EnableUnitFile(u system.Unit) error { func (tum *TestUnitManager) EnableUnitFile(u system.Unit) error {
tum.enabled = append(tum.enabled, u.Name) tum.enabled = append(tum.enabled, u.Name)
return nil return nil
@ -122,6 +126,69 @@ func TestProcessUnits(t *testing.T) {
enabled: []string{"woof"}, enabled: []string{"woof"},
}, },
}, },
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "hi.service",
Runtime: true,
Content: "[Service]\nExecStart=/bin/echo hi",
DropIns: []config.UnitDropIn{
{
Name: "lo.conf",
Content: "[Service]\nExecStart=/bin/echo lo",
},
{
Name: "bye.conf",
Content: "[Service]\nExecStart=/bin/echo bye",
},
},
}},
},
result: TestUnitManager{
placed: []string{"hi.service", "hi.service.d/lo.conf", "hi.service.d/bye.conf"},
unmasked: []string{"hi.service"},
reload: true,
},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
DropIns: []config.UnitDropIn{
{
Name: "lo.conf",
Content: "[Service]\nExecStart=/bin/echo lo",
},
},
}},
},
result: TestUnitManager{},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "hi.service",
DropIns: []config.UnitDropIn{
{
Content: "[Service]\nExecStart=/bin/echo lo",
},
},
}},
},
result: TestUnitManager{},
},
{
units: []system.Unit{
system.Unit{Unit: config.Unit{
Name: "hi.service",
DropIns: []config.UnitDropIn{
{
Name: "lo.conf",
},
},
}},
},
result: TestUnitManager{},
},
} }
for _, tt := range tests { for _, tt := range tests {

View File

@ -25,7 +25,7 @@ import (
// dropinContents generates the contents for a drop-in unit given the config. // dropinContents generates the contents for a drop-in unit given the config.
// The argument must be a struct from the 'config' package. // The argument must be a struct from the 'config' package.
func dropinContents(e interface{}) string { func serviceContents(e interface{}) string {
et := reflect.TypeOf(e) et := reflect.TypeOf(e)
ev := reflect.ValueOf(e) ev := reflect.ValueOf(e)
@ -42,16 +42,3 @@ func dropinContents(e interface{}) string {
} }
return "[Service]\n" + out return "[Service]\n" + out
} }
func dropinFromConfig(cfg interface{}, name string) []Unit {
content := dropinContents(cfg)
if content == "" {
return nil
}
return []Unit{{config.Unit{
Name: name,
Runtime: true,
DropIn: true,
Content: content,
}}}
}

View File

@ -4,7 +4,7 @@ import (
"testing" "testing"
) )
func TestDropinContents(t *testing.T) { func TestServiceContents(t *testing.T) {
tests := []struct { tests := []struct {
Config interface{} Config interface{}
Contents string Contents string
@ -48,7 +48,7 @@ Environment="D=0.1"
} }
for _, tt := range tests { for _, tt := range tests {
if c := dropinContents(tt.Config); c != tt.Contents { if c := serviceContents(tt.Config); c != tt.Contents {
t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.Contents, c) t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.Contents, c)
} }
} }

View File

@ -28,5 +28,12 @@ type Etcd struct {
// Units creates a Unit file drop-in for etcd, using any configured options. // Units creates a Unit file drop-in for etcd, using any configured options.
func (ee Etcd) Units() []Unit { func (ee Etcd) Units() []Unit {
return dropinFromConfig(ee.Etcd, "etcd.service") return []Unit{{config.Unit{
Name: "etcd.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: serviceContents(ee.Etcd),
}},
}}}
} }

View File

@ -30,7 +30,11 @@ func TestEtcdUnits(t *testing.T) {
}{ }{
{ {
config.Etcd{}, config.Etcd{},
nil, []Unit{{config.Unit{
Name: "etcd.service",
Runtime: true,
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
}}},
}, },
{ {
config.Etcd{ config.Etcd{
@ -40,11 +44,13 @@ func TestEtcdUnits(t *testing.T) {
[]Unit{{config.Unit{ []Unit{{config.Unit{
Name: "etcd.service", Name: "etcd.service",
Runtime: true, Runtime: true,
DropIn: true, DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: `[Service] Content: `[Service]
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar" Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002" Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`, `,
}},
}}}, }}},
}, },
{ {
@ -56,12 +62,14 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
[]Unit{{config.Unit{ []Unit{{config.Unit{
Name: "etcd.service", Name: "etcd.service",
Runtime: true, Runtime: true,
DropIn: true, DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: `[Service] Content: `[Service]
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar" Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
Environment="ETCD_NAME=node001" Environment="ETCD_NAME=node001"
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002" Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
`, `,
}},
}}}, }}},
}, },
} { } {

View File

@ -13,5 +13,12 @@ type Flannel struct {
// Units generates a Unit file drop-in for flannel, if any flannel options were // Units generates a Unit file drop-in for flannel, if any flannel options were
// configured in cloud-config // configured in cloud-config
func (fl Flannel) Units() []Unit { func (fl Flannel) Units() []Unit {
return dropinFromConfig(fl.Flannel, "flanneld.service") return []Unit{{config.Unit{
Name: "flanneld.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: serviceContents(fl.Flannel),
}},
}}}
} }

View File

@ -14,7 +14,11 @@ func TestFlannelUnits(t *testing.T) {
}{ }{
{ {
config.Flannel{}, config.Flannel{},
nil, []Unit{{config.Unit{
Name: "flanneld.service",
Runtime: true,
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
}}},
}, },
{ {
config.Flannel{ config.Flannel{
@ -23,12 +27,14 @@ func TestFlannelUnits(t *testing.T) {
}, },
[]Unit{{config.Unit{ []Unit{{config.Unit{
Name: "flanneld.service", Name: "flanneld.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: `[Service] Content: `[Service]
Environment="FLANNELD_ETCD_ENDPOINT=http://12.34.56.78:4001" Environment="FLANNELD_ETCD_ENDPOINT=http://12.34.56.78:4001"
Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network/tenant1" Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network/tenant1"
`, `,
Runtime: true, }},
DropIn: true,
}}}, }}},
}, },
} { } {

View File

@ -29,5 +29,12 @@ type Fleet struct {
// Units generates a Unit file drop-in for fleet, if any fleet options were // Units generates a Unit file drop-in for fleet, if any fleet options were
// configured in cloud-config // configured in cloud-config
func (fe Fleet) Units() []Unit { func (fe Fleet) Units() []Unit {
return dropinFromConfig(fe.Fleet, "fleet.service") return []Unit{{config.Unit{
Name: "fleet.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: serviceContents(fe.Fleet),
}},
}}}
} }

View File

@ -30,7 +30,11 @@ func TestFleetUnits(t *testing.T) {
}{ }{
{ {
config.Fleet{}, config.Fleet{},
nil, []Unit{{config.Unit{
Name: "fleet.service",
Runtime: true,
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
}}},
}, },
{ {
config.Fleet{ config.Fleet{
@ -38,11 +42,13 @@ func TestFleetUnits(t *testing.T) {
}, },
[]Unit{{config.Unit{ []Unit{{config.Unit{
Name: "fleet.service", Name: "fleet.service",
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: `[Service] Content: `[Service]
Environment="FLEET_PUBLIC_IP=12.34.56.78" Environment="FLEET_PUBLIC_IP=12.34.56.78"
`, `,
Runtime: true, }},
DropIn: true,
}}}, }}},
}, },
} { } {

View File

@ -54,6 +54,19 @@ func (s *systemd) PlaceUnit(u Unit) error {
return err return err
} }
// PlaceUnitDropIn writes a unit drop-in file at its desired destination,
// creating parent directories as necessary.
func (s *systemd) PlaceUnitDropIn(u Unit, d config.UnitDropIn) error {
file := File{config.File{
Path: u.DropInDestination(s.root, d),
Content: d.Content,
RawFilePermissions: "0644",
}}
_, err := WriteFile(&file, "/")
return err
}
func (s *systemd) EnableUnitFile(u Unit) error { func (s *systemd) EnableUnitFile(u Unit) error {
conn, err := dbus.New() conn, err := dbus.New()
if err != nil { if err != nil {

View File

@ -74,6 +74,64 @@ func TestPlaceUnit(t *testing.T) {
} }
} }
func TestPlaceUnitDropIn(t *testing.T) {
tests := []config.Unit{
{
Name: "false.service",
Runtime: true,
DropIns: []config.UnitDropIn{
{
Name: "00-true.conf",
Content: "[Service]\nExecStart=\nExecStart=/usr/bin/true\n",
},
},
},
{
Name: "true.service",
DropIns: []config.UnitDropIn{
{
Name: "00-false.conf",
Content: "[Service]\nExecStart=\nExecStart=/usr/bin/false\n",
},
},
},
}
for _, tt := range tests {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
panic(fmt.Sprintf("Unable to create tempdir: %v", err))
}
u := Unit{tt}
sd := &systemd{dir}
if err := sd.PlaceUnitDropIn(u, u.DropIns[0]); err != nil {
t.Fatalf("PlaceUnit(): bad error (%+v): want nil, got %s", tt, err)
}
fi, err := os.Stat(u.DropInDestination(dir, u.DropIns[0]))
if err != nil {
t.Fatalf("Stat(): bad error (%+v): want nil, got %s", tt, err)
}
if mode := fi.Mode(); mode != os.FileMode(0644) {
t.Errorf("bad filemode (%+v): want %v, got %v", tt, os.FileMode(0644), mode)
}
c, err := ioutil.ReadFile(u.DropInDestination(dir, u.DropIns[0]))
if err != nil {
t.Fatalf("ReadFile(): bad error (%+v): want nil, got %s", tt, err)
}
if string(c) != u.DropIns[0].Content {
t.Errorf("bad contents (%+v): want %q, got %q", tt, u.DropIns[0].Content, string(c))
}
os.RemoveAll(dir)
}
}
func TestMachineID(t *testing.T) { func TestMachineID(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil { if err != nil {

View File

@ -25,11 +25,9 @@ import (
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
) )
// Name for drop-in service configuration files created by cloudconfig
const cloudConfigDropIn = "20-cloudinit.conf"
type UnitManager interface { type UnitManager interface {
PlaceUnit(unit Unit) error PlaceUnit(unit Unit) error
PlaceUnitDropIn(unit Unit, dropIn config.UnitDropIn) error
EnableUnitFile(unit Unit) error EnableUnitFile(unit Unit) error
RunUnitCommand(unit Unit, command string) (string, error) RunUnitCommand(unit Unit, command string) (string, error)
MaskUnit(unit Unit) error MaskUnit(unit Unit) error
@ -66,14 +64,21 @@ func (u Unit) Group() string {
// argument indicates the effective base directory of the system (similar to a // argument indicates the effective base directory of the system (similar to a
// chroot). // chroot).
func (u Unit) Destination(root string) string { func (u Unit) Destination(root string) string {
return path.Join(u.prefix(root), u.Name)
}
// DropInDestination builds the appropriate absolute file path for the
// UnitDropIn. The root argument indicates the effective base directory of the
// system (similar to a chroot) and the dropIn argument is the UnitDropIn for
// which the destination is being calculated.
func (u Unit) DropInDestination(root string, dropIn config.UnitDropIn) string {
return path.Join(u.prefix(root), fmt.Sprintf("%s.d", u.Name), dropIn.Name)
}
func (u Unit) prefix(root string) string {
dir := "etc" dir := "etc"
if u.Runtime { if u.Runtime {
dir = "run" dir = "run"
} }
return path.Join(root, dir, "systemd", u.Group())
if u.DropIn {
return path.Join(root, dir, "systemd", u.Group(), fmt.Sprintf("%s.d", u.Name), cloudConfigDropIn)
} else {
return path.Join(root, dir, "systemd", u.Group(), u.Name)
}
} }

View File

@ -98,3 +98,41 @@ func TestDestination(t *testing.T) {
} }
} }
} }
func TestDropInDestination(t *testing.T) {
tests := []struct {
root string
unitName string
dropInName string
runtime bool
destination string
}{
{
root: "/some/dir",
unitName: "foo.service",
dropInName: "bar.conf",
destination: "/some/dir/etc/systemd/system/foo.service.d/bar.conf",
},
{
root: "/some/dir",
unitName: "foo.service",
dropInName: "bar.conf",
runtime: true,
destination: "/some/dir/run/systemd/system/foo.service.d/bar.conf",
},
}
for _, tt := range tests {
u := Unit{config.Unit{
Name: tt.unitName,
Runtime: tt.runtime,
DropIns: []config.UnitDropIn{{
Name: tt.dropInName,
}},
}}
if d := u.DropInDestination(tt.root, u.DropIns[0]); tt.destination != d {
t.Errorf("bad destination (%+v): want %q, got %q", tt, tt.destination, d)
}
}
}