diff --git a/cloudinit/cloud_config.go b/cloudinit/cloud_config.go index a61d338..e0b49fe 100644 --- a/cloudinit/cloud_config.go +++ b/cloudinit/cloud_config.go @@ -10,7 +10,11 @@ const DefaultSSHKeyName = "coreos-cloudinit" type CloudConfig struct { SSH_Authorized_Keys []string - Coreos struct{Etcd struct{ Discovery_URL string }; Fleet struct{ Autostart bool } } + Coreos struct { + Etcd struct{ Discovery_URL string } + Fleet struct{ Autostart bool } + } + Write_Files []WriteFile } func NewCloudConfig(contents []byte) (*CloudConfig, error) { @@ -38,6 +42,15 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error { } } + if len(cfg.Write_Files) > 0 { + for _, file := range cfg.Write_Files { + if err := ProcessWriteFile("/", &file); err != nil { + return err + } + log.Printf("Wrote file %s to filesystem", file.Path) + } + } + if cfg.Coreos.Etcd.Discovery_URL != "" { err := PersistEtcdDiscoveryURL(cfg.Coreos.Etcd.Discovery_URL) if err == nil { diff --git a/cloudinit/cloud_config_test.go b/cloudinit/cloud_config_test.go index c002402..fe75f4b 100644 --- a/cloudinit/cloud_config_test.go +++ b/cloudinit/cloud_config_test.go @@ -23,6 +23,10 @@ func TestCloudConfigEmpty(t *testing.T) { if cfg.Coreos.Fleet.Autostart { t.Error("Expected AutostartFleet not to be defined") } + + if len(cfg.Write_Files) != 0 { + t.Error("Expected zero Write_Files") + } } // Assert that the parsing of a cloud config file "generally works" @@ -36,6 +40,13 @@ coreos: ssh_authorized_keys: - foobar - foobaz +write_files: + - content: | + penny + elroy + path: /etc/dogepack.conf + permissions: '0644' + owner: root:dogepack `) cfg, err := NewCloudConfig(contents) if err != nil { @@ -58,6 +69,27 @@ ssh_authorized_keys: if !cfg.Coreos.Fleet.Autostart { t.Error("Expected AutostartFleet to be true") } + + if len(cfg.Write_Files) != 1 { + t.Error("Failed to parse correct number of write_files") + } else { + wf := cfg.Write_Files[0] + if wf.Content != "penny\nelroy\n" { + t.Errorf("WriteFile has incorrect contents '%s'", wf.Content) + } + if wf.Encoding != "" { + t.Errorf("WriteFile has incorrect encoding %s", wf.Encoding) + } + if wf.Permissions != "0644" { + t.Errorf("WriteFile has incorrect permissions %s", wf.Permissions) + } + if wf.Path != "/etc/dogepack.conf" { + t.Errorf("WriteFile has incorrect path %s", wf.Path) + } + if wf.Owner != "root:dogepack" { + t.Errorf("WriteFile has incorrect owner %s", wf.Owner) + } + } } // Assert that our interface conversion doesn't panic diff --git a/cloudinit/write_file.go b/cloudinit/write_file.go new file mode 100644 index 0000000..601b725 --- /dev/null +++ b/cloudinit/write_file.go @@ -0,0 +1,46 @@ +package cloudinit + +import ( + "errors" + "io/ioutil" + "os" + "os/exec" + "path" + "strconv" +) + +type WriteFile struct { + Encoding string + Content string + Owner string + Path string + Permissions string +} + +func ProcessWriteFile(base string, wf *WriteFile) error { + fullPath := path.Join(base, wf.Path) + + if err := os.MkdirAll(path.Dir(fullPath), os.FileMode(0744)); err != nil { + return err + } + + // Parse string representation of file mode as octal + perm, err := strconv.ParseInt(wf.Permissions, 8, 32) + if err != nil { + return errors.New("Unable to parse file permissions as octal integer") + } + + if err := ioutil.WriteFile(fullPath, []byte(wf.Content), os.FileMode(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 +} diff --git a/cloudinit/write_file_test.go b/cloudinit/write_file_test.go new file mode 100644 index 0000000..7111f4a --- /dev/null +++ b/cloudinit/write_file_test.go @@ -0,0 +1,81 @@ +package cloudinit + +import ( + "io/ioutil" + "os" + "path" + "syscall" + "testing" +) + +func TestWriteFileUnencodedContent(t *testing.T) { + wf := WriteFile{ + Path: "/tmp/foo", + Content: "bar", + Permissions: "0644", + } + 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") + + 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) + } + + if string(contents) != "bar" { + t.Fatalf("File has incorrect contents") + } +} + +func TestWriteFileInvalidPermission(t *testing.T) { + wf := WriteFile{ + Path: "/tmp/foo", + Content: "bar", + Permissions: "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 { + t.Fatalf("Expected error to be raised when writing file with invalid permission") + } +} + +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 { + t.Fatalf("Expected error to be raised when writing file with encoding") + } +}