refactor(*): Break apart packages

This commit is contained in:
Brian Waldon 2014-03-18 09:00:41 -07:00
parent 5185fe48da
commit d2dabee0c6
21 changed files with 330 additions and 262 deletions

View File

@ -1,30 +0,0 @@
package cloudinit
import (
"bufio"
"bytes"
"fmt"
"log"
"strings"
)
func ParseUserData(contents []byte) (interface{}, error) {
bytereader := bytes.NewReader(contents)
bufreader := bufio.NewReader(bytereader)
header, _ := bufreader.ReadString('\n')
if strings.HasPrefix(header, "#!") {
log.Printf("Parsing user-data as script")
return Script(contents), nil
} else if header == "#cloud-config\n" {
log.Printf("Parsing user-data as cloud-config")
cfg, err := NewCloudConfig(contents)
if err != nil {
log.Fatal(err.Error())
}
return *cfg, nil
} else {
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
}
}

View File

@ -1,66 +0,0 @@
package cloudinit
import (
"fmt"
"io/ioutil"
"os"
"path"
)
func PrepWorkspace(workspace string) error {
// Ensure workspace exists and is a directory
info, err := os.Stat(workspace)
if err == nil {
if !info.IsDir() {
return fmt.Errorf("%s is not a directory", workspace)
}
} else {
err = os.MkdirAll(workspace, 0755)
if err != nil {
return err
}
}
// Ensure scripts dir in workspace exists and is a directory
scripts := path.Join(workspace, "scripts")
info, err = os.Stat(scripts)
if err == nil {
if !info.IsDir() {
return fmt.Errorf("%s is not a directory", scripts)
}
} else {
err = os.Mkdir(scripts, 0755)
if err != nil {
return err
}
}
return nil
}
func PersistScriptInWorkspace(script Script, workspace string) (string, error) {
scriptsDir := path.Join(workspace, "scripts")
f, err := ioutil.TempFile(scriptsDir, "")
if err != nil {
return "", err
}
defer f.Close()
f.Chmod(0744)
_, err = f.Write(script)
if err != nil {
return "", err
}
// Ensure script has been written to disk before returning, as the
// next natural thing to do is execute it
f.Sync()
return f.Name(), nil
}
func PersistScriptUnitNameInWorkspace(name string, workspace string) error {
unitPath := path.Join(workspace, "scripts", "unit-name")
return ioutil.WriteFile(unitPath, []byte(name), 0644)
}

View File

@ -1,63 +0,0 @@
package cloudinit
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
)
type WriteFile struct {
Encoding string
Content string
Owner string
Path string
RawFilePermissions string `yaml:"permissions"`
}
func (wf *WriteFile) Permissions() (os.FileMode, error) {
if wf.RawFilePermissions == "" {
return os.FileMode(0644), nil
}
// Parse string representation of file mode as octal
perm, err := strconv.ParseInt(wf.RawFilePermissions, 8, 32)
if err != nil {
return 0, errors.New("Unable to parse file permissions as octal integer")
}
return os.FileMode(perm), nil
}
func ProcessWriteFile(base string, wf *WriteFile) error {
if wf.Encoding != "" {
return fmt.Errorf("Unable to write file with encoding %s", wf.Encoding)
}
fullPath := path.Join(base, wf.Path)
if err := os.MkdirAll(path.Dir(fullPath), os.FileMode(0755)); err != nil {
return err
}
perm, err := wf.Permissions()
if err != nil {
return err
}
if err := ioutil.WriteFile(fullPath, []byte(wf.Content), perm); err != nil {
return err
}
if wf.Owner != "" {
// We shell out since we don't have a way to look up unix groups natively
cmd := exec.Command("chown", wf.Owner, fullPath)
if err := cmd.Run(); err != nil {
return err
}
}
return nil
}

View File

@ -1,21 +1,22 @@
package main
import (
"fmt"
"bufio"
"bytes"
"flag"
"io/ioutil"
"os"
"fmt"
"log"
"os"
"strings"
"github.com/coreos/coreos-cloudinit/cloudinit"
"github.com/coreos/coreos-cloudinit/datasource"
"github.com/coreos/coreos-cloudinit/initialize"
"github.com/coreos/coreos-cloudinit/system"
)
const version = "0.1.2+git"
func main() {
var userdata []byte
var err error
var printVersion bool
flag.BoolVar(&printVersion, "version", false, "Print the version and exit")
@ -29,7 +30,7 @@ func main() {
flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
var sshKeyName string
flag.StringVar(&sshKeyName, "ssh-key-name", cloudinit.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
flag.StringVar(&sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
flag.Parse()
@ -43,49 +44,48 @@ func main() {
os.Exit(1)
}
var ds datasource.Datasource
if file != "" {
log.Printf("Reading user-data from file: %s", file)
userdata, err = ioutil.ReadFile(file)
if err != nil {
log.Fatal(err.Error())
}
ds = datasource.NewLocalFile(file)
} else if url != "" {
log.Printf("Reading user-data from metadata service")
svc := cloudinit.NewMetadataService(url)
userdata, err = svc.UserData()
if err != nil {
log.Fatal(err.Error())
}
ds = datasource.NewMetadataService(url)
} else {
fmt.Println("Provide one of --from-file or --from-url")
os.Exit(1)
}
log.Printf("Fetching user-data from datasource of type %q", ds.Type())
userdata, err := ds.Fetch()
if err != nil {
log.Fatalf("Failed fetching user-data from datasource: %v", err)
}
if len(userdata) == 0 {
log.Printf("No user data to handle, exiting.")
os.Exit(0)
}
parsed, err := cloudinit.ParseUserData(userdata)
parsed, err := ParseUserData(userdata)
if err != nil {
log.Fatalf("Failed parsing user-data: %v", err)
}
err = cloudinit.PrepWorkspace(workspace)
env := initialize.NewEnvironment("/", workspace)
err = initialize.PrepWorkspace(env.Workspace())
if err != nil {
log.Fatalf("Failed preparing workspace: %v", err)
}
switch t := parsed.(type) {
case cloudinit.CloudConfig:
err = cloudinit.ApplyCloudConfig(t, sshKeyName)
case cloudinit.Script:
case initialize.CloudConfig:
err = initialize.Apply(t, env)
case system.Script:
var path string
path, err = cloudinit.PersistScriptInWorkspace(t, workspace)
path, err = initialize.PersistScriptInWorkspace(t, env.Workspace())
if err == nil {
var name string
name, err = cloudinit.ExecuteScript(path)
cloudinit.PersistScriptUnitNameInWorkspace(name, workspace)
name, err = system.ExecuteScript(path)
initialize.PersistUnitNameInWorkspace(name, workspace)
}
}
@ -93,3 +93,24 @@ func main() {
log.Fatalf("Failed resolving user-data: %v", err)
}
}
func ParseUserData(contents []byte) (interface{}, error) {
bytereader := bytes.NewReader(contents)
bufreader := bufio.NewReader(bytereader)
header, _ := bufreader.ReadString('\n')
if strings.HasPrefix(header, "#!") {
log.Printf("Parsing user-data as script")
return system.Script(contents), nil
} else if header == "#cloud-config\n" {
log.Printf("Parsing user-data as cloud-config")
cfg, err := initialize.NewCloudConfig(contents)
if err != nil {
log.Fatal(err.Error())
}
return *cfg, nil
} else {
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
}
}

6
datasource/datasource.go Normal file
View File

@ -0,0 +1,6 @@
package datasource
type Datasource interface {
Fetch() ([]byte, error)
Type() string
}

21
datasource/file.go Normal file
View File

@ -0,0 +1,21 @@
package datasource
import (
"io/ioutil"
)
type localFile struct {
path string
}
func NewLocalFile(path string) *localFile {
return &localFile{path}
}
func (self *localFile) Fetch() ([]byte, error) {
return ioutil.ReadFile(self.path)
}
func (self *localFile) Type() string {
return "local-file"
}

View File

@ -1,4 +1,4 @@
package cloudinit
package datasource
import (
"io/ioutil"
@ -14,7 +14,7 @@ func NewMetadataService(url string) *metadataService {
return &metadataService{url, http.Client{}}
}
func (ms *metadataService) UserData() ([]byte, error) {
func (ms *metadataService) Fetch() ([]byte, error) {
resp, err := ms.client.Get(ms.url)
if err != nil {
return []byte{}, err
@ -33,4 +33,6 @@ func (ms *metadataService) UserData() ([]byte, error) {
return respBytes, nil
}
func (ms *metadataService) Type() string {
return "metadata-service"
}

View File

@ -1,24 +1,25 @@
package cloudinit
package initialize
import (
"fmt"
"log"
"path"
"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml"
)
const DefaultSSHKeyName = "coreos-cloudinit"
"github.com/coreos/coreos-cloudinit/system"
)
type CloudConfig struct {
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
Coreos struct {
Etcd EtcdEnvironment
Fleet struct{ Autostart bool }
Units []Unit
Units []system.Unit
}
WriteFiles []WriteFile `yaml:"write_files"`
WriteFiles []system.File `yaml:"write_files"`
Hostname string
Users []User
Users []system.User
}
func NewCloudConfig(contents []byte) (*CloudConfig, error) {
@ -39,9 +40,9 @@ func (cc CloudConfig) String() string {
return stringified
}
func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
func Apply(cfg CloudConfig, env *Environment) error {
if cfg.Hostname != "" {
if err := SetHostname(cfg.Hostname); err != nil {
if err := system.SetHostname(cfg.Hostname); err != nil {
return err
}
log.Printf("Set hostname to %s", cfg.Hostname)
@ -54,18 +55,18 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
continue
}
if UserExists(&user) {
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 := SetUserPassword(user.Name, user.PasswordHash); err != nil {
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 := CreateUser(&user); err != nil {
if err := system.CreateUser(&user); err != nil {
log.Printf("Failed creating user '%s': %v", user.Name, err)
return err
}
@ -73,7 +74,7 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
if len(user.SSHAuthorizedKeys) > 0 {
log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name)
if err := AuthorizeSSHKeys(user.Name, sshKeyName, user.SSHAuthorizedKeys); err != nil {
if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil {
return err
}
}
@ -81,7 +82,7 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
}
if len(cfg.SSHAuthorizedKeys) > 0 {
err := AuthorizeSSHKeys("core", sshKeyName, cfg.SSHAuthorizedKeys)
err := system.AuthorizeSSHKeys("core", env.SSHKeyName(), cfg.SSHAuthorizedKeys)
if err == nil {
log.Printf("Authorized SSH keys for core user")
} else {
@ -91,7 +92,8 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
if len(cfg.WriteFiles) > 0 {
for _, file := range cfg.WriteFiles {
if err := ProcessWriteFile("/", &file); err != nil {
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)
@ -99,7 +101,7 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
}
if len(cfg.Coreos.Etcd) > 0 {
if err := WriteEtcdEnvironment("/", cfg.Coreos.Etcd); err != nil {
if err := WriteEtcdEnvironment(cfg.Coreos.Etcd, env.Root()); err != nil {
log.Fatalf("Failed to write etcd config to filesystem: %v", err)
}
@ -109,7 +111,7 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
if len(cfg.Coreos.Units) > 0 {
for _, unit := range cfg.Coreos.Units {
log.Printf("Placing unit %s on filesystem", unit.Name)
dst, err := PlaceUnit("/", &unit)
dst, err := system.PlaceUnit(&unit, env.Root())
if err != nil {
return err
}
@ -117,7 +119,7 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
if unit.Group() != "network" {
log.Printf("Enabling unit file %s", dst)
if err := EnableUnitFile(dst, unit.Runtime); err != nil {
if err := system.EnableUnitFile(dst, unit.Runtime); err != nil {
return err
}
log.Printf("Enabled unit %s", unit.Name)
@ -125,12 +127,12 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
log.Printf("Skipping enable for network-like unit %s", unit.Name)
}
}
DaemonReload()
StartUnits(cfg.Coreos.Units)
system.DaemonReload()
system.StartUnits(cfg.Coreos.Units)
}
if cfg.Coreos.Fleet.Autostart {
err := StartUnitByName("fleet.service")
err := system.StartUnitByName("fleet.service")
if err == nil {
log.Printf("Started fleet service.")
} else {

View File

@ -1,4 +1,4 @@
package cloudinit
package initialize
import (
"strings"

33
initialize/env.go Normal file
View File

@ -0,0 +1,33 @@
package initialize
import (
"path"
)
const DefaultSSHKeyName = "coreos-cloudinit"
type Environment struct {
root string
workspace string
sshKeyName string
}
func NewEnvironment(root, workspace string) *Environment {
return &Environment{root, workspace, DefaultSSHKeyName}
}
func (self *Environment) Workspace() string {
return path.Join(self.root, self.workspace)
}
func (self *Environment) Root() string {
return self.root
}
func (self *Environment) SSHKeyName() string {
return self.sshKeyName
}
func (self *Environment) SetSSHKeyName(name string) {
self.sshKeyName = name
}

View File

@ -1,11 +1,12 @@
package cloudinit
package initialize
import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"github.com/coreos/coreos-cloudinit/system"
)
type EtcdEnvironment map[string]string
@ -34,15 +35,12 @@ func (ec EtcdEnvironment) String() (out string) {
}
// 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
}
func WriteEtcdEnvironment(env EtcdEnvironment, root string) error {
file := system.File{
Path: path.Join(root, "etc", "systemd", "system", "etcd.service.d", "20-cloudinit.conf"),
RawFilePermissions: "0644",
Content: env.String(),
}
return ioutil.WriteFile(cfgFile, []byte(env.String()), os.FileMode(0644))
return system.WriteFile(&file)
}

View File

@ -1,4 +1,4 @@
package cloudinit
package initialize
import (
"io/ioutil"
@ -55,7 +55,7 @@ func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
}
defer syscall.Rmdir(dir)
if err := WriteEtcdEnvironment(dir, ec); err != nil {
if err := WriteEtcdEnvironment(ec, dir); err != nil {
t.Fatalf("Processing of EtcdEnvironment failed: %v", err)
}

48
initialize/workspace.go Normal file
View File

@ -0,0 +1,48 @@
package initialize
import (
"io/ioutil"
"path"
"github.com/coreos/coreos-cloudinit/system"
)
func PrepWorkspace(workspace string) error {
if err := system.EnsureDirectoryExists(workspace); err != nil {
return err
}
scripts := path.Join(workspace, "scripts")
if err := system.EnsureDirectoryExists(scripts); err != nil {
return err
}
return nil
}
func PersistScriptInWorkspace(script system.Script, workspace string) (string, error) {
scriptsPath := path.Join(workspace, "scripts")
tmp, err := ioutil.TempFile(scriptsPath, "")
if err != nil {
return "", err
}
tmp.Close()
file := system.File{
Path: tmp.Name(),
RawFilePermissions: "0744",
Content: string(script),
}
err = system.WriteFile(&file)
return file.Path, err
}
func PersistUnitNameInWorkspace(name string, workspace string) error {
file := system.File{
Path: path.Join(workspace, "scripts", "unit-name"),
RawFilePermissions: "0644",
Content: name,
}
return system.WriteFile(&file)
}

77
system/file.go Normal file
View File

@ -0,0 +1,77 @@
package system
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
)
type File struct {
Encoding string
Content string
Owner string
Path string
RawFilePermissions string `yaml:"permissions"`
}
func (f *File) Permissions() (os.FileMode, error) {
if f.RawFilePermissions == "" {
return os.FileMode(0644), nil
}
// Parse string representation of file mode as octal
perm, err := strconv.ParseInt(f.RawFilePermissions, 8, 32)
if err != nil {
return 0, errors.New("Unable to parse file permissions as octal integer")
}
return os.FileMode(perm), nil
}
func WriteFile(f *File) error {
if f.Encoding != "" {
return fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
}
if err := os.MkdirAll(path.Dir(f.Path), os.FileMode(0755)); err != nil {
return err
}
perm, err := f.Permissions()
if err != nil {
return err
}
if err := ioutil.WriteFile(f.Path, []byte(f.Content), perm); err != nil {
return err
}
if f.Owner != "" {
// We shell out since we don't have a way to look up unix groups natively
cmd := exec.Command("chown", f.Owner, f.Path)
if err := cmd.Run(); err != nil {
return err
}
}
return nil
}
func EnsureDirectoryExists(dir string) error {
info, err := os.Stat(dir)
if err == nil {
if !info.IsDir() {
return fmt.Errorf("%s is not a directory", dir)
}
} else {
err = os.MkdirAll(dir, 0755)
if err != nil {
return err
}
}
return nil
}

View File

@ -1,4 +1,4 @@
package cloudinit
package system
import (
"io/ioutil"
@ -9,21 +9,23 @@ import (
)
func TestWriteFileUnencodedContent(t *testing.T) {
wf := WriteFile{
Path: "/tmp/foo",
Content: "bar",
}
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 := ProcessWriteFile(dir, &wf); err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err)
fullPath := path.Join(dir, "tmp", "foo")
wf := File{
Path: fullPath,
Content: "bar",
RawFilePermissions: "0644",
}
fullPath := path.Join(dir, "tmp", "foo")
if err := WriteFile(&wf); err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err)
}
fi, err := os.Stat(fullPath)
if err != nil {
@ -45,38 +47,40 @@ func TestWriteFileUnencodedContent(t *testing.T) {
}
func TestWriteFileInvalidPermission(t *testing.T) {
wf := WriteFile{
Path: "/tmp/foo",
Content: "bar",
RawFilePermissions: "pants",
}
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 := ProcessWriteFile(dir, &wf); err == nil {
wf := File{
Path: path.Join(dir, "tmp", "foo"),
Content: "bar",
RawFilePermissions: "pants",
}
if err := WriteFile(&wf); err == nil {
t.Fatalf("Expected error to be raised when writing file with invalid permission")
}
}
func TestWriteFilePermissions(t *testing.T) {
wf := WriteFile{
Path: "/tmp/foo",
RawFilePermissions: "0755",
}
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 := ProcessWriteFile(dir, &wf); err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err)
fullPath := path.Join(dir, "tmp", "foo")
wf := File{
Path: fullPath,
RawFilePermissions: "0755",
}
fullPath := path.Join(dir, "tmp", "foo")
if err := WriteFile(&wf); err != nil {
t.Fatalf("Processing of WriteFile failed: %v", err)
}
fi, err := os.Stat(fullPath)
if err != nil {
@ -89,19 +93,19 @@ func TestWriteFilePermissions(t *testing.T) {
}
func TestWriteFileEncodedContent(t *testing.T) {
wf := WriteFile{
Path: "/tmp/foo",
Content: "",
Encoding: "base64",
}
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 := ProcessWriteFile(dir, &wf); err == nil {
wf := File{
Path: path.Join(dir, "tmp", "foo"),
Content: "",
Encoding: "base64",
}
if err := WriteFile(&wf); err == nil {
t.Fatalf("Expected error to be raised when writing file with encoding")
}
}

View File

@ -1,4 +1,4 @@
package cloudinit
package system
import (
"fmt"

View File

@ -1,8 +1,7 @@
package cloudinit
package system
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
@ -36,7 +35,7 @@ func (u *Unit) Group() (group string) {
type Script []byte
func PlaceUnit(root string, u *Unit) (string, error) {
func PlaceUnit(u *Unit, root string) (string, error) {
dir := "etc"
if u.Runtime {
dir = "run"
@ -50,7 +49,14 @@ func PlaceUnit(root string, u *Unit) (string, error) {
}
dst = path.Join(dst, u.Name)
err := ioutil.WriteFile(dst, []byte(u.Content), os.FileMode(0644))
file := File{
Path: dst,
Content: u.Content,
RawFilePermissions: "0644",
}
err := WriteFile(&file)
if err != nil {
return "", err
}

View File

@ -1,4 +1,4 @@
package cloudinit
package system
import (
"io/ioutil"
@ -26,7 +26,7 @@ Address=10.209.171.177/19
}
defer syscall.Rmdir(dir)
if _, err := PlaceUnit(dir, &u); err != nil {
if _, err := PlaceUnit(&u, dir); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}
@ -72,7 +72,7 @@ Where=/media/state
}
defer syscall.Rmdir(dir)
if _, err := PlaceUnit(dir, &u); err != nil {
if _, err := PlaceUnit(&u, dir); err != nil {
t.Fatalf("PlaceUnit failed: %v", err)
}

View File

@ -1,4 +1,4 @@
package cloudinit
package system
import (
"fmt"

6
test
View File

@ -4,5 +4,7 @@ echo "Building bin/coreos-cloudinit"
. build
echo "Running tests..."
go test -i github.com/coreos/coreos-cloudinit/cloudinit
go test -v github.com/coreos/coreos-cloudinit/cloudinit
for pkg in "./initialize ./system"; do
go test -i $pkg
go test -v $pkg
done

7
test.yaml Normal file
View File

@ -0,0 +1,7 @@
#cloud-config
coreos:
etcd:
discovery_url: https://discovery.etcd.io/0022cb5027f8f5167a874794c3a13e0d
bind-addr: $public_ipv4:4001
name: polvi