Merge pull request #100 from jonboulle/rework
refactor(*): rework cloudconfig for better extensibility and consistency
This commit is contained in:
		| @@ -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,54 +71,45 @@ 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 | ||||
| 	for _, user := range cfg.Users { | ||||
| 		if user.Name == "" { | ||||
| 			log.Printf("User object has no 'name' field, skipping") | ||||
| 			continue | ||||
| 		} | ||||
| 		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") | ||||
| 				continue | ||||
| 		if system.UserExists(&user) { | ||||
| 			log.Printf("User '%s' exists, ignoring creation-time fields", user.Name) | ||||
| 			if user.PasswordHash != "" { | ||||
| 				log.Printf("Setting '%s' user's password", user.Name) | ||||
| 				if err := system.SetUserPassword(user.Name, user.PasswordHash); err != nil { | ||||
| 					log.Printf("Failed setting '%s' user's password: %v", user.Name, err) | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
| 		} else { | ||||
| 			log.Printf("Creating user '%s'", user.Name) | ||||
| 			if err := system.CreateUser(&user); err != nil { | ||||
| 				log.Printf("Failed creating user '%s': %v", user.Name, err) | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 			if system.UserExists(&user) { | ||||
| 				log.Printf("User '%s' exists, ignoring creation-time fields", user.Name) | ||||
| 				if user.PasswordHash != "" { | ||||
| 					log.Printf("Setting '%s' user's password", user.Name) | ||||
| 					if err := system.SetUserPassword(user.Name, user.PasswordHash); err != nil { | ||||
| 						log.Printf("Failed setting '%s' user's password: %v", user.Name, err) | ||||
| 						return err | ||||
| 					} | ||||
| 				} | ||||
| 			} else { | ||||
| 				log.Printf("Creating user '%s'", user.Name) | ||||
| 				if err := system.CreateUser(&user); err != nil { | ||||
| 					log.Printf("Failed creating user '%s': %v", user.Name, err) | ||||
| 					return err | ||||
| 				} | ||||
| 		if len(user.SSHAuthorizedKeys) > 0 { | ||||
| 			log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name) | ||||
| 			if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			if len(user.SSHAuthorizedKeys) > 0 { | ||||
| 				log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name) | ||||
| 				if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 		} | ||||
| 		if user.SSHImportGithubUser != "" { | ||||
| 			log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name) | ||||
| 			if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			if user.SSHImportGithubUser != "" { | ||||
| 				log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name) | ||||
| 				if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
| 			if user.SSHImportURL != "" { | ||||
| 				log.Printf("Authorizing SSH keys for CoreOS user '%s' from '%s'", user.Name, user.SSHImportURL) | ||||
| 				if err := SSHImportKeysFromURL(user.Name, user.SSHImportURL); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 		} | ||||
| 		if user.SSHImportURL != "" { | ||||
| 			log.Printf("Authorizing SSH keys for CoreOS user '%s' from '%s'", user.Name, user.SSHImportURL) | ||||
| 			if err := SSHImportKeysFromURL(user.Name, user.SSHImportURL); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| @@ -111,86 +123,86 @@ func Apply(cfg CloudConfig, env *Environment) error { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(cfg.WriteFiles) > 0 { | ||||
| 		for _, file := range cfg.WriteFiles { | ||||
| 			file.Path = path.Join(env.Root(), file.Path) | ||||
| 			if err := system.WriteFile(&file); err != nil { | ||||
| 	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 { | ||||
| 			return err | ||||
| 		} | ||||
| 		log.Printf("Wrote file %s to filesystem", file.Path) | ||||
| 	} | ||||
|  | ||||
| 	commands := make(map[string]string, 0) | ||||
| 	reload := false | ||||
| 	for _, unit := range cfg.Coreos.Units { | ||||
| 		dst := system.UnitDestination(&unit, env.Root()) | ||||
| 		if unit.Content != "" { | ||||
| 			log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst) | ||||
| 			if err := system.PlaceUnit(&unit, dst); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			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("Placed unit %s at %s", unit.Name, dst) | ||||
| 			reload = true | ||||
| 		} | ||||
|  | ||||
| 		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) | ||||
| 		if unit.Mask { | ||||
| 			log.Printf("Masking unit file %s", unit.Name) | ||||
| 			if err := system.MaskUnit(unit.Name, env.Root()); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 		log.Printf("Wrote locksmith config file to filesystem") | ||||
| 	} | ||||
|  | ||||
| 	if len(cfg.Coreos.Units) > 0 { | ||||
| 		commands := make(map[string]string, 0) | ||||
| 		for _, unit := range cfg.Coreos.Units { | ||||
| 			dst := system.UnitDestination(&unit, env.Root()) | ||||
| 			if unit.Content != "" { | ||||
| 				log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst) | ||||
| 				if err := system.PlaceUnit(&unit, dst); err != nil { | ||||
| 		if unit.Enable { | ||||
| 			if unit.Group() != "network" { | ||||
| 				log.Printf("Enabling unit file %s", dst) | ||||
| 				if err := system.EnableUnitFile(dst, unit.Runtime); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 				log.Printf("Placed unit %s at %s", unit.Name, dst) | ||||
| 			} | ||||
|  | ||||
| 			if unit.Enable { | ||||
| 				if unit.Group() != "network" { | ||||
| 					log.Printf("Enabling unit file %s", dst) | ||||
| 					if err := system.EnableUnitFile(dst, unit.Runtime); err != nil { | ||||
| 						return err | ||||
| 					} | ||||
| 					log.Printf("Enabled unit %s", unit.Name) | ||||
| 				} else { | ||||
| 					log.Printf("Skipping enable for network-like unit %s", unit.Name) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if unit.Group() == "network" { | ||||
| 				commands["systemd-networkd.service"] = "restart" | ||||
| 				log.Printf("Enabled unit %s", unit.Name) | ||||
| 			} else { | ||||
| 				if unit.Command != "" { | ||||
| 					commands[unit.Name] = unit.Command | ||||
| 				} | ||||
| 				log.Printf("Skipping enable for network-like unit %s", unit.Name) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if err := system.DaemonReload(); err != nil { | ||||
| 			log.Fatalf("Failed systemd daemon-reload: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		for unit, command := range commands { | ||||
| 			log.Printf("Calling unit command '%s %s'", command, unit) | ||||
| 			res, err := system.RunUnitCommand(command, unit) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			log.Printf("Result of '%s %s': %s", command, unit, res) | ||||
| 		if unit.Group() == "network" { | ||||
| 			commands["systemd-networkd.service"] = "restart" | ||||
| 		} else if unit.Command != "" { | ||||
| 			commands[unit.Name] = unit.Command | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if cfg.ManageEtcHosts != "" { | ||||
|  | ||||
| 		if err := WriteEtcHosts(cfg.ManageEtcHosts, env.Root()); err != nil { | ||||
| 			log.Fatalf("Failed to write /etc/hosts to filesystem: %v", err) | ||||
| 	if reload { | ||||
| 		if err := system.DaemonReload(); err != nil { | ||||
| 			return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 		log.Printf("Wrote /etc/hosts file to filesystem") | ||||
|  | ||||
| 	for unit, command := range commands { | ||||
| 		log.Printf("Calling unit command '%s %s'", command, unit) | ||||
| 		res, err := system.RunUnitCommand(command, unit) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		log.Printf("Result of '%s %s': %s", command, unit, res) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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() | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| 		} | ||||
| 		cmd = "stop" | ||||
| 	} else { | ||||
| 		return addStrategy(strategy, root) | ||||
| // 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, | ||||
| 	} | ||||
| 	if err := system.DaemonReload(); err != nil { | ||||
| 		return err | ||||
|  | ||||
| 	if uc.strategy() == "off" { | ||||
| 		u.Enable = false | ||||
| 		u.Command = "stop" | ||||
| 		u.Mask = true | ||||
| 	} | ||||
| 	if _, err := system.RunUnitCommand(cmd, locksmithUnit); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
|  | ||||
| 	return u, nil | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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" | ||||
| 	} | ||||
|  | ||||
| 	return path.Join(root, dir, "systemd", u.Group(), u.Name) | ||||
| 	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 | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user