Merge pull request #100 from jonboulle/rework

refactor(*): rework cloudconfig for better extensibility and consistency
This commit is contained in:
Jonathan Boulle 2014-05-14 11:39:53 -07:00
commit 32c52d8729
13 changed files with 366 additions and 231 deletions

View File

@ -1,6 +1,7 @@
package initialize
import (
"errors"
"fmt"
"log"
"path"
@ -10,18 +11,35 @@ import (
"github.com/coreos/coreos-cloudinit/system"
)
// CloudConfigFile represents a CoreOS specific configuration option that can generate
// an associated system.File to be written to disk
type CloudConfigFile interface {
// File should either return (*system.File, error), or (nil, nil) if nothing
// needs to be done for this configuration option.
File(root string) (*system.File, error)
}
// CloudConfigUnit represents a CoreOS specific configuration option that can generate
// an associated system.Unit to be created/enabled appropriately
type CloudConfigUnit interface {
// Unit should either return (*system.Unit, error), or (nil, nil) if nothing
// needs to be done for this configuration option.
Unit(root string) (*system.Unit, error)
}
// CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML
type CloudConfig struct {
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
Coreos struct {
Etcd EtcdEnvironment
Update map[string]string
Units []system.Unit
OEM OEMRelease
Update UpdateConfig
Units []system.Unit
}
WriteFiles []system.File `yaml:"write_files"`
Hostname string
Users []system.User
ManageEtcHosts string `yaml:"manage_etc_hosts"`
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
}
func NewCloudConfig(contents string) (*CloudConfig, error) {
@ -42,6 +60,9 @@ func (cc CloudConfig) String() string {
return stringified
}
// Apply renders a CloudConfig to an Environment. This can involve things like
// configuring the hostname, adding new users, writing various configuration
// files to disk, and manipulating systemd services.
func Apply(cfg CloudConfig, env *Environment) error {
if cfg.Hostname != "" {
if err := system.SetHostname(cfg.Hostname); err != nil {
@ -50,14 +71,6 @@ func Apply(cfg CloudConfig, env *Environment) error {
log.Printf("Set hostname to %s", cfg.Hostname)
}
if cfg.Coreos.OEM.ID != "" {
if err := WriteOEMRelease(&cfg.Coreos.OEM, env.Root()); err != nil {
return err
}
log.Printf("Wrote /etc/oem-release to filesystem")
}
if len(cfg.Users) > 0 {
for _, user := range cfg.Users {
if user.Name == "" {
log.Printf("User object has no 'name' field, skipping")
@ -100,7 +113,6 @@ func Apply(cfg CloudConfig, env *Environment) error {
}
}
}
}
if len(cfg.SSHAuthorizedKeys) > 0 {
err := system.AuthorizeSSHKeys("core", env.SSHKeyName(), cfg.SSHAuthorizedKeys)
@ -111,7 +123,26 @@ func Apply(cfg CloudConfig, env *Environment) error {
}
}
if len(cfg.WriteFiles) > 0 {
for _, ccf := range []CloudConfigFile{cfg.Coreos.OEM, cfg.Coreos.Update, cfg.ManageEtcHosts} {
f, err := ccf.File(env.Root())
if err != nil {
return err
}
if f != nil {
cfg.WriteFiles = append(cfg.WriteFiles, *f)
}
}
for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Update} {
u, err := ccu.Unit(env.Root())
if err != nil {
return err
}
if u != nil {
cfg.Coreos.Units = append(cfg.Coreos.Units, *u)
}
}
for _, file := range cfg.WriteFiles {
file.Path = path.Join(env.Root(), file.Path)
if err := system.WriteFile(&file); err != nil {
@ -119,25 +150,9 @@ func Apply(cfg CloudConfig, env *Environment) error {
}
log.Printf("Wrote file %s to filesystem", file.Path)
}
}
if len(cfg.Coreos.Etcd) > 0 {
if err := WriteEtcdEnvironment(cfg.Coreos.Etcd, env.Root()); err != nil {
log.Fatalf("Failed to write etcd config to filesystem: %v", err)
}
log.Printf("Wrote etcd config file to filesystem")
}
if s, ok := cfg.Coreos.Update["reboot-strategy"]; ok {
if err := WriteLocksmithConfig(s, env.Root()); err != nil {
log.Fatalf("Failed to write locksmith config to filesystem: %v", err)
}
log.Printf("Wrote locksmith config file to filesystem")
}
if len(cfg.Coreos.Units) > 0 {
commands := make(map[string]string, 0)
reload := false
for _, unit := range cfg.Coreos.Units {
dst := system.UnitDestination(&unit, env.Root())
if unit.Content != "" {
@ -146,6 +161,14 @@ func Apply(cfg CloudConfig, env *Environment) error {
return err
}
log.Printf("Placed unit %s at %s", unit.Name, dst)
reload = true
}
if unit.Mask {
log.Printf("Masking unit file %s", unit.Name)
if err := system.MaskUnit(unit.Name, env.Root()); err != nil {
return err
}
}
if unit.Enable {
@ -162,15 +185,15 @@ func Apply(cfg CloudConfig, env *Environment) error {
if unit.Group() == "network" {
commands["systemd-networkd.service"] = "restart"
} else {
if unit.Command != "" {
} else if unit.Command != "" {
commands[unit.Name] = unit.Command
}
}
}
if reload {
if err := system.DaemonReload(); err != nil {
log.Fatalf("Failed systemd daemon-reload: %v", err)
return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err))
}
}
for unit, command := range commands {
@ -181,17 +204,6 @@ func Apply(cfg CloudConfig, env *Environment) error {
}
log.Printf("Result of '%s %s': %s", command, unit, res)
}
}
if cfg.ManageEtcHosts != "" {
if err := WriteEtcHosts(cfg.ManageEtcHosts, env.Root()); err != nil {
log.Fatalf("Failed to write /etc/hosts to filesystem: %v", err)
}
log.Printf("Wrote /etc/hosts file to filesystem")
}
return nil
}

View File

@ -144,7 +144,7 @@ ssh_authorized_keys:
`
cfg, err := NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error :%v", err)
t.Fatalf("Encountered unexpected error: %v", err)
}
keys := cfg.SSHAuthorizedKeys
@ -162,6 +162,26 @@ 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) {
contents := `
users:

View File

@ -45,3 +45,16 @@ func (self *Environment) Apply(data string) string {
}
return data
}
// normalizeSvcEnv standardizes the keys of the map (environment variables for a service)
// by replacing any dashes with underscores and ensuring they are entirely upper case.
// For example, "some-env" --> "SOME_ENV"
func normalizeSvcEnv(m map[string]string) map[string]string {
out := make(map[string]string, len(m))
for key, val := range m {
key = strings.ToUpper(key)
key = strings.Replace(key, "-", "_", -1)
out[key] = val
}
return out
}

View File

@ -3,26 +3,14 @@ package initialize
import (
"errors"
"fmt"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
type EtcdEnvironment map[string]string
func (ec EtcdEnvironment) normalized() map[string]string {
out := make(map[string]string, len(ec))
for key, val := range ec {
key = strings.ToUpper(key)
key = strings.Replace(key, "-", "_", -1)
out[key] = val
}
return out
}
func (ec EtcdEnvironment) String() (out string) {
norm := ec.normalized()
func (ee EtcdEnvironment) String() (out string) {
norm := normalizeSvcEnv(ee)
if val, ok := norm["DISCOVERY_URL"]; ok {
delete(norm, "DISCOVERY_URL")
@ -40,23 +28,23 @@ func (ec EtcdEnvironment) String() (out string) {
return
}
// Write an EtcdEnvironment to the appropriate path on disk for etcd.service
func WriteEtcdEnvironment(env EtcdEnvironment, root string) error {
if _, ok := env["name"]; !ok {
// Unit creates a Unit file drop-in for etcd, using any configured
// options and adding a default MachineID if unset.
func (ee EtcdEnvironment) Unit(root string) (*system.Unit, error) {
if _, ok := ee["name"]; !ok {
if machineID := system.MachineID(root); machineID != "" {
env["name"] = machineID
ee["name"] = machineID
} else if hostname, err := system.Hostname(); err == nil {
env["name"] = hostname
ee["name"] = hostname
} else {
return errors.New("Unable to determine default etcd name")
return nil, errors.New("Unable to determine default etcd name")
}
}
file := system.File{
Path: path.Join(root, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf"),
RawFilePermissions: "0644",
Content: env.String(),
}
return system.WriteFile(&file)
return &system.Unit{
Name: "etcd.service",
Runtime: true,
DropIn: true,
Content: ee.String(),
}, nil
}

View File

@ -3,9 +3,10 @@ package initialize
import (
"io/ioutil"
"os"
"os/exec"
"path"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestEtcdEnvironment(t *testing.T) {
@ -69,8 +70,18 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
}
defer os.RemoveAll(dir)
if err := WriteEtcdEnvironment(ec, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
u, err := ec.Unit(dir)
if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err)
}
if u == nil {
t.Fatalf("Returned nil etcd unit unexpectedly")
}
dst := system.UnitDestination(u, dir)
os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
}
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
@ -100,7 +111,7 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
}
func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
ec := EtcdEnvironment{}
ee := EtcdEnvironment{}
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
@ -113,8 +124,18 @@ func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
t.Fatalf("Failed writing out /etc/machine-id: %v", err)
}
if err := WriteEtcdEnvironment(ec, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
u, err := ee.Unit(dir)
if err != nil {
t.Fatalf("Generating etcd unit failed: %v", err)
}
if u == nil {
t.Fatalf("Returned nil etcd unit unexpectedly")
}
dst := system.UnitDestination(u, dir)
os.Stderr.WriteString("writing to " + dir + "\n")
if err := system.PlaceUnit(u, dst); err != nil {
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
}
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
@ -131,8 +152,3 @@ Environment="ETCD_NAME=node007"
t.Fatalf("File has incorrect contents")
}
}
func rmdir(path string) error {
cmd := exec.Command("rm", "-rf", path)
return cmd.Run()
}

View File

@ -2,8 +2,6 @@ package initialize
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
@ -13,73 +11,79 @@ import (
const locksmithUnit = "locksmithd.service"
// addStrategy creates an `/etc/coreos/update.conf` file with the requested
// strategy via rewriting the file on disk or by starting from
// `/usr/share/coreos/update.conf`.
func addStrategy(strategy string, root string) error {
type UpdateConfig map[string]string
func (uc UpdateConfig) strategy() string {
s, _ := uc["reboot-strategy"]
return s
}
// File creates an `/etc/coreos/update.conf` file with the requested
// strategy, by either rewriting the existing file on disk, or starting
// from `/usr/share/coreos/update.conf`
func (uc UpdateConfig) File(root string) (*system.File, error) {
// If no reboot-strategy is set, we don't need to generate a new config
if _, ok := uc["reboot-strategy"]; !ok {
return nil, nil
}
var out string
etcUpdate := path.Join(root, "etc", "coreos", "update.conf")
usrUpdate := path.Join(root, "usr", "share", "coreos", "update.conf")
// Ensure /etc/coreos/ exists before attempting to write a file in it
os.MkdirAll(path.Join(root, "etc", "coreos"), 0755)
tmp, err := ioutil.TempFile(path.Join(root, "etc", "coreos"), ".update.conf")
if err != nil {
return err
}
err = tmp.Chmod(0644)
if err != nil {
return err
}
conf, err := os.Open(etcUpdate)
if os.IsNotExist(err) {
conf, err = os.Open(usrUpdate)
if err != nil {
return err
}
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(conf)
sawStrat := false
stratLine := "REBOOT_STRATEGY="+strategy
stratLine := "REBOOT_STRATEGY=" + uc.strategy()
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "REBOOT_STRATEGY=") {
line = stratLine
sawStrat = true
}
fmt.Fprintln(tmp, line)
out += line
out += "\n"
if err := scanner.Err(); err != nil {
return err
return nil, err
}
}
if !sawStrat {
fmt.Fprintln(tmp, stratLine)
out += stratLine
out += "\n"
}
return os.Rename(tmp.Name(), etcUpdate)
return &system.File{
Path: path.Join("etc", "coreos", "update.conf"),
RawFilePermissions: "0644",
Content: out,
}, nil
}
// WriteLocksmithConfig updates the `update.conf` file with a REBOOT_STRATEGY for locksmith.
func WriteLocksmithConfig(strategy string, root string) error {
cmd := "restart"
if strategy == "off" {
err := system.MaskUnit(locksmithUnit, root)
if err != nil {
return err
// Unit generates a locksmith system.Unit for the cloud-init initializer to
// act on appropriately
func (uc UpdateConfig) Unit(root string) (*system.Unit, error) {
u := &system.Unit{
Name: locksmithUnit,
Enable: true,
Command: "restart",
Mask: false,
}
cmd = "stop"
} else {
return addStrategy(strategy, root)
if uc.strategy() == "off" {
u.Enable = false
u.Command = "stop"
u.Mask = true
}
if err := system.DaemonReload(); err != nil {
return err
}
if _, err := system.RunUnitCommand(cmd, locksmithUnit); err != nil {
return err
}
return nil
return u, nil
}

View File

@ -5,6 +5,8 @@ import (
"os"
"path"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
const (
@ -42,9 +44,19 @@ func TestLocksmithEnvironmentWrittenToDisk(t *testing.T) {
t.Fatal(err)
}
}
uc := &UpdateConfig{"reboot-strategy": "etcd-lock"}
if err := WriteLocksmithConfig("etcd-lock", dir); err != nil {
t.Fatalf("Processing of LocksmithEnvironment failed: %v", err)
f, err := uc.File(dir)
if err != nil {
t.Fatalf("Processing UpdateConfig failed: %v", err)
}
if f == nil {
t.Fatalf("UpdateConfig generated nil file unexpectedly")
}
f.Path = path.Join(dir, f.Path)
if err := system.WriteFile(f); err != nil {
t.Fatalf("Error writing update config: %v", err)
}
fullPath := path.Join(dir, "etc", "coreos", "update.conf")
@ -76,9 +88,17 @@ func TestLocksmithEnvironmentMasked(t *testing.T) {
defer os.RemoveAll(dir)
setupFixtures(dir)
if err := WriteLocksmithConfig("off", dir); err != nil {
t.Fatalf("Processing of LocksmithEnvironment failed: %v", err)
uc := &UpdateConfig{"reboot-strategy": "off"}
u, err := uc.Unit(dir)
if err != nil {
t.Fatalf("Processing UpdateConfig failed: %v", err)
}
if u == nil {
t.Fatalf("UpdateConfig generated nil unit unexpectedly")
}
system.MaskUnit(u.Name, dir)
fullPath := path.Join(dir, "etc", "systemd", "system", "locksmithd.service")
target, err := os.Readlink(fullPath)

View File

@ -11,8 +11,10 @@ import (
const DefaultIpv4Address = "127.0.0.1"
func generateEtcHosts(option string) (out string, err error) {
if option != "localhost" {
type EtcHosts string
func (eh EtcHosts) generateEtcHosts() (out string, err error) {
if eh != "localhost" {
return "", errors.New("Invalid option to manage_etc_hosts")
}
@ -26,19 +28,19 @@ func generateEtcHosts(option string) (out string, err error) {
}
// Write an /etc/hosts file
func WriteEtcHosts(option string, root string) error {
etcHosts, err := generateEtcHosts(option)
if err != nil {
return err
func (eh EtcHosts) File(root string) (*system.File, error) {
if eh == "" {
return nil, nil
}
file := system.File{
Path: path.Join(root, "etc", "hosts"),
etcHosts, err := eh.generateEtcHosts()
if err != nil {
return nil, err
}
return &system.File{
Path: path.Join("etc", "hosts"),
RawFilePermissions: "0644",
Content: etcHosts,
}
return system.WriteFile(&file)
}, nil
}

View File

@ -6,6 +6,8 @@ import (
"os"
"path"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestCloudConfigManageEtcHosts(t *testing.T) {
@ -25,14 +27,9 @@ manage_etc_hosts: localhost
}
func TestManageEtcHostsInvalidValue(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer rmdir(dir)
if err := WriteEtcHosts("invalid", dir); err == nil {
t.Fatalf("WriteEtcHosts succeeded with invalid value: %v", err)
eh := EtcHosts("invalid")
if f, err := eh.File(""); err == nil || f != nil {
t.Fatalf("EtcHosts File succeeded with invalid value!")
}
}
@ -41,10 +38,22 @@ func TestEtcHostsWrittenToDisk(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create tempdir: %v", err)
}
defer rmdir(dir)
defer os.RemoveAll(dir)
if err := WriteEtcHosts("localhost", dir); err != nil {
t.Fatalf("WriteEtcHosts failed: %v", err)
eh := EtcHosts("localhost")
f, err := eh.File(dir)
if err != nil {
t.Fatalf("Error calling File on EtcHosts: %v", err)
}
if f == nil {
t.Fatalf("manageEtcHosts returned nil file unexpectedly")
}
f.Path = path.Join(dir, f.Path)
if err := system.WriteFile(f); err != nil {
t.Fatalf("Error writing EtcHosts: %v", err)
}
fullPath := path.Join(dir, "etc", "hosts")

View File

@ -16,7 +16,7 @@ type OEMRelease struct {
BugReportURL string `yaml:"bug-report-url"`
}
func (oem *OEMRelease) String() string {
func (oem OEMRelease) String() string {
fields := []string{
fmt.Sprintf("ID=%s", oem.ID),
fmt.Sprintf("VERSION_ID=%s", oem.VersionID),
@ -28,12 +28,14 @@ func (oem *OEMRelease) String() string {
return strings.Join(fields, "\n") + "\n"
}
func WriteOEMRelease(oem *OEMRelease, root string) error {
file := system.File{
Path: path.Join(root, "etc", "oem-release"),
RawFilePermissions: "0644",
Content: oem.String(),
func (oem OEMRelease) File(root string) (*system.File, error) {
if oem.ID == "" {
return nil, nil
}
return system.WriteFile(&file)
return &system.File{
Path: path.Join("etc", "oem-release"),
RawFilePermissions: "0644",
Content: oem.String(),
}, nil
}

View File

@ -5,6 +5,8 @@ import (
"os"
"path"
"testing"
"github.com/coreos/coreos-cloudinit/system"
)
func TestOEMReleaseWrittenToDisk(t *testing.T) {
@ -21,8 +23,17 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) {
}
defer os.RemoveAll(dir)
if err := WriteOEMRelease(&oem, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
f, err := oem.File(dir)
if err != nil {
t.Fatalf("Processing of OEMRelease failed: %v", err)
}
if f == nil {
t.Fatalf("OEMRelease returned nil file unexpectedly")
}
f.Path = path.Join(dir, f.Path)
if err := system.WriteFile(f); err != nil {
t.Fatalf("Writing of OEMRelease failed: %v", err)
}
fullPath := path.Join(dir, "etc", "oem-release")

View File

@ -17,12 +17,21 @@ import (
// never be used as a true MachineID
const fakeMachineID = "42000000000000000000000000000042"
// Name for drop-in service configuration files created by cloudconfig
const cloudConfigDropIn = "20-cloudinit.conf"
type Unit struct {
Name string
Mask bool
Enable bool
Runtime bool
Content string
Command string
// 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.
DropIn bool `yaml:"-"`
}
func (u *Unit) Type() string {
@ -42,8 +51,8 @@ func (u *Unit) Group() (group string) {
type Script []byte
// UnitDestination builds the appropriate absolte file path for
// the given unit. The root argument indicates the effective base
// UnitDestination builds the appropriate absolute file path for
// the given Unit. The root argument indicates the effective base
// directory of the system (similar to a chroot).
func UnitDestination(u *Unit, root string) string {
dir := "etc"
@ -51,7 +60,11 @@ func UnitDestination(u *Unit, root string) string {
dir = "run"
}
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)
}
}
// PlaceUnit writes a unit file at the provided destination, creating

View File

@ -60,6 +60,30 @@ Address=10.209.171.177/19
}
}
func TestUnitDestination(t *testing.T) {
dir := "/some/dir"
name := "foobar.service"
u := Unit{
Name: name,
DropIn: false,
}
dst := UnitDestination(&u, dir)
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
if dst != expectDst {
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst)
}
u.DropIn = true
dst = UnitDestination(&u, dir)
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
if dst != expectDst {
t.Errorf("UnitDestination returned %s, expected %s", dst, expectDst)
}
}
func TestPlaceMountUnit(t *testing.T) {
u := Unit{
Name: "media-state.mount",
@ -123,6 +147,7 @@ func TestMachineID(t *testing.T) {
t.Fatalf("File has incorrect contents")
}
}
func TestMaskUnit(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
if err != nil {