From 137949f5ad6d79b443239b17104e8c6e32257458 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 17 Mar 2014 15:09:59 -0700 Subject: [PATCH] feat(etcd): Write etcd systemd snippet --- Documentation/cloud-config.md | 34 +++++++++++-- cloudinit/cloud_config.go | 13 +++-- cloudinit/cloud_config_test.go | 8 --- cloudinit/etcd.go | 45 ++++++++++++----- cloudinit/etcd_test.go | 90 ++++++++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 30 deletions(-) create mode 100644 cloudinit/etcd_test.go diff --git a/Documentation/cloud-config.md b/Documentation/cloud-config.md index 0e0049c..05b63ab 100644 --- a/Documentation/cloud-config.md +++ b/Documentation/cloud-config.md @@ -74,12 +74,38 @@ Provide a list of objects with the following attributes: ## Custom cloud-config Parameters -### coreos.etcd.discovery_url +### coreos.etcd -The value of `coreos.etcd.discovery_url` will be used to discover the instance's etcd peers using the [etcd discovery protocol][disco-proto]. Usage of the [public discovery service][disco-service] is encouraged. +The `coreos.etcd.*` options are translated to a partial systemd unit acting as an etcd configuration file. +`coreos-cloudinit` will also replace the strings `$private_ipv4` and `$public_ipv4` with the values generated by CoreOS based on a given provider. -[disco-proto]: https://github.com/coreos/etcd/blob/master/Documentation/discovery-protocol.md -[disco-service]: http://discovery.etcd.io +For example, the following cloud-config document... + +``` +#cloud-config + +coreos: + etcd: + name: node001 + discovery-url: https://discovery.etcd.io/3445fa65423d8b04df07f59fb40218f8 + bind-addr: $public_ipv4:4001 + peer-bind-addr: $private_ipv4:7001 +``` + +...will generate a systemd snippet like this: + +``` +[Service] +Environment="ETCD_NAME=node001"" +Environment="ETCD_DISCOVERY_URL=https://discovery.etcd.io/3445fa65423d8b04df07f59fb40218f8" +Environment="ETCD_BIND_ADDR=203.0.113.29:4001" +Environment="ETCD_PEER_BIND_ADDR=192.0.2.13:7001" +``` + +For more information about the available configuration options, see the [etcd documentation][etcd-config]. +Note that hyphens in the coreos.etcd.* keys are mapped to underscores. + +[etcd-config]: https://github.com/coreos/etcd/blob/master/Documentation/configuration.md ### coreos.units diff --git a/cloudinit/cloud_config.go b/cloudinit/cloud_config.go index aa36507..a9cb817 100644 --- a/cloudinit/cloud_config.go +++ b/cloudinit/cloud_config.go @@ -12,7 +12,7 @@ const DefaultSSHKeyName = "coreos-cloudinit" type CloudConfig struct { SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"` Coreos struct { - Etcd struct{ Discovery_URL string } + Etcd EtcdEnvironment Fleet struct{ Autostart bool } Units []Unit } @@ -98,13 +98,12 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error { } } - if cfg.Coreos.Etcd.Discovery_URL != "" { - err := PersistEtcdDiscoveryURL(cfg.Coreos.Etcd.Discovery_URL) - if err == nil { - log.Printf("Consumed etcd discovery url") - } else { - log.Fatalf("Failed to persist etcd discovery url to filesystem: %v", err) + if len(cfg.Coreos.Etcd) > 0 { + if err := WriteEtcdEnvironment("/", cfg.Coreos.Etcd); err != nil { + log.Fatalf("Failed to write etcd config to filesystem: %v", err) } + + log.Printf("Wrote etcd config file to filesystem") } if len(cfg.Coreos.Units) > 0 { diff --git a/cloudinit/cloud_config_test.go b/cloudinit/cloud_config_test.go index c7c540e..81158f2 100644 --- a/cloudinit/cloud_config_test.go +++ b/cloudinit/cloud_config_test.go @@ -17,10 +17,6 @@ func TestCloudConfigEmpty(t *testing.T) { t.Error("Parsed incorrect number of SSH keys") } - if cfg.Coreos.Etcd.Discovery_URL != "" { - t.Error("Parsed incorrect value of discovery url") - } - if cfg.Coreos.Fleet.Autostart { t.Error("Expected AutostartFleet not to be defined") } @@ -81,10 +77,6 @@ hostname: trontastic t.Error("Expected first SSH key to be 'foobaz'") } - if cfg.Coreos.Etcd.Discovery_URL != "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877" { - t.Error("Failed to parse etcd discovery url") - } - if !cfg.Coreos.Fleet.Autostart { t.Error("Expected AutostartFleet to be true") } diff --git a/cloudinit/etcd.go b/cloudinit/etcd.go index 1e0498e..229bd41 100644 --- a/cloudinit/etcd.go +++ b/cloudinit/etcd.go @@ -1,25 +1,48 @@ package cloudinit import ( + "fmt" "io/ioutil" - "log" "os" "path" + "strings" ) -const ( - etcdDiscoveryPath = "/var/run/etcd/bootstrap.disco" -) +type EtcdEnvironment map[string]string -func PersistEtcdDiscoveryURL(url string) error { - dir := path.Dir(etcdDiscoveryPath) - if _, err := os.Stat(dir); err != nil { - log.Printf("Creating directory /var/run/etcd") - err := os.MkdirAll(dir, os.FileMode(0644)) - if err != nil { +func (ec EtcdEnvironment) String() (out string) { + public := os.Getenv("COREOS_PUBLIC_IPV4") + private := os.Getenv("COREOS_PRIVATE_IPV4") + + out += "[Service]\n" + + for key, val := range ec { + key = strings.ToUpper(key) + key = strings.Replace(key, "-", "_", -1) + + if public != "" { + val = strings.Replace(val, "$public_ipv4", public, -1) + } + + if private != "" { + val = strings.Replace(val, "$private_ipv4", private, -1) + } + + out += fmt.Sprintf("Environment=\"ETCD_%s=%s\"\n", key, val) + } + return +} + +// Write an EtcdEnvironment to the appropriate path on disk for etcd.service +func WriteEtcdEnvironment(root string, env EtcdEnvironment) error { + cfgDir := path.Join(root, "etc", "systemd", "system", "etcd.service.d") + cfgFile := path.Join(cfgDir, "20-cloudinit.conf") + + if _, err := os.Stat(cfgDir); err != nil { + if err := os.MkdirAll(cfgDir, os.FileMode(0755)); err != nil { return err } } - return ioutil.WriteFile(etcdDiscoveryPath, []byte(url), os.FileMode(0644)) + return ioutil.WriteFile(cfgFile, []byte(env.String()), os.FileMode(0644)) } diff --git a/cloudinit/etcd_test.go b/cloudinit/etcd_test.go new file mode 100644 index 0000000..c76abc7 --- /dev/null +++ b/cloudinit/etcd_test.go @@ -0,0 +1,90 @@ +package cloudinit + +import ( + "io/ioutil" + "os" + "os/exec" + "path" + "syscall" + "testing" +) + +func TestEtcdEnvironment(t *testing.T) { + cfg := make(EtcdEnvironment, 0) + cfg["discovery_url"] = "http://disco.example.com/foobar" + cfg["peer-bind-addr"] = "127.0.0.1:7002" + + env := cfg.String() + expect := `[Service] +Environment="ETCD_DISCOVERY_URL=http://disco.example.com/foobar" +Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002" +` + + if env != expect { + t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect) + } +} + +func TestEtcdEnvironmentReplacement(t *testing.T) { + os.Clearenv() + os.Setenv("COREOS_PUBLIC_IPV4", "203.0.113.29") + os.Setenv("COREOS_PRIVATE_IPV4", "192.0.2.13") + + cfg := make(EtcdEnvironment, 0) + cfg["bind-addr"] = "$public_ipv4:4001" + cfg["peer-bind-addr"] = "$private_ipv4:7001" + + env := cfg.String() + expect := `[Service] +Environment="ETCD_BIND_ADDR=203.0.113.29:4001" +Environment="ETCD_PEER_BIND_ADDR=192.0.2.13:7001" +` + if env != expect { + t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect) + } +} + +func TestEtcdEnvironmentWrittenToDisk(t *testing.T) { + ec := EtcdEnvironment{ + "discovery_url": "http://disco.example.com/foobar", + "peer-bind-addr": "127.0.0.1:7002", + } + dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-") + if err != nil { + t.Fatalf("Unable to create tempdir: %v", err) + } + defer syscall.Rmdir(dir) + + if err := WriteEtcdEnvironment(dir, ec); err != nil { + t.Fatalf("Processing of EtcdEnvironment failed: %v", err) + } + + fullPath := path.Join(dir, "etc", "systemd", "system", "etcd.service.d", "20-cloudinit.conf") + + fi, err := os.Stat(fullPath) + if err != nil { + t.Fatalf("Unable to stat file: %v", err) + } + + if fi.Mode() != os.FileMode(0644) { + t.Errorf("File has incorrect mode: %v", fi.Mode()) + } + + contents, err := ioutil.ReadFile(fullPath) + if err != nil { + t.Fatalf("Unable to read expected file: %v", err) + } + + expect := `[Service] +Environment="ETCD_DISCOVERY_URL=http://disco.example.com/foobar" +Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002" +` + if string(contents) != expect { + t.Fatalf("File has incorrect contents") + } +} + +func rmdir(path string) error { + cmd := exec.Command("rm", "-rf", path) + return cmd.Run() +}