Merge pull request #140 from jonboulle/atomic
fix(system): write all files atomically
This commit is contained in:
		| @@ -4,7 +4,6 @@ import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"path" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/third_party/launchpad.net/goyaml" | ||||
|  | ||||
| @@ -221,11 +220,11 @@ func Apply(cfg CloudConfig, env *Environment) error { | ||||
| 	} | ||||
|  | ||||
| 	for _, file := range cfg.WriteFiles { | ||||
| 		file.Path = path.Join(env.Root(), file.Path) | ||||
| 		if err := system.WriteFile(&file); err != nil { | ||||
| 		path, err := system.WriteFile(&file, env.Root()) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		log.Printf("Wrote file %s to filesystem", file.Path) | ||||
| 		log.Printf("Wrote file %s to filesystem", path) | ||||
| 	} | ||||
|  | ||||
| 	commands := make(map[string]string, 0) | ||||
|   | ||||
| @@ -50,9 +50,7 @@ func TestEtcHostsWrittenToDisk(t *testing.T) { | ||||
| 		t.Fatalf("manageEtcHosts returned nil file unexpectedly") | ||||
| 	} | ||||
|  | ||||
| 	f.Path = path.Join(dir, f.Path) | ||||
|  | ||||
| 	if err := system.WriteFile(f); err != nil { | ||||
| 	if _, err := system.WriteFile(f, dir); err != nil { | ||||
| 		t.Fatalf("Error writing EtcHosts: %v", err) | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -31,8 +31,7 @@ func TestOEMReleaseWrittenToDisk(t *testing.T) { | ||||
| 		t.Fatalf("OEMRelease returned nil file unexpectedly") | ||||
| 	} | ||||
|  | ||||
| 	f.Path = path.Join(dir, f.Path) | ||||
| 	if err := system.WriteFile(f); err != nil { | ||||
| 	if _, err := system.WriteFile(f, dir); err != nil { | ||||
| 		t.Fatalf("Writing of OEMRelease failed: %v", err) | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -205,8 +205,7 @@ func TestUpdateConfWrittenToDisk(t *testing.T) { | ||||
| 			t.Fatal("Unexpectedly got nil updateconfig file") | ||||
| 		} | ||||
|  | ||||
| 		f.Path = path.Join(dir, f.Path) | ||||
| 		if err := system.WriteFile(f); err != nil { | ||||
| 		if _, err := system.WriteFile(f, dir); err != nil { | ||||
| 			t.Fatalf("Error writing update config: %v", err) | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package initialize | ||||
| import ( | ||||
| 	"io/ioutil" | ||||
| 	"path" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/system" | ||||
| ) | ||||
| @@ -28,21 +29,23 @@ func PersistScriptInWorkspace(script system.Script, workspace string) (string, e | ||||
| 	} | ||||
| 	tmp.Close() | ||||
|  | ||||
| 	relpath := strings.TrimPrefix(tmp.Name(), workspace) | ||||
|  | ||||
| 	file := system.File{ | ||||
| 		Path: tmp.Name(), | ||||
| 		Path:               relpath, | ||||
| 		RawFilePermissions: "0744", | ||||
| 		Content: string(script), | ||||
| 		Content:            string(script), | ||||
| 	} | ||||
|  | ||||
| 	err = system.WriteFile(&file) | ||||
| 	return file.Path, err | ||||
| 	return system.WriteFile(&file, workspace) | ||||
| } | ||||
|  | ||||
| func PersistUnitNameInWorkspace(name string, workspace string) error { | ||||
| 	file := system.File{ | ||||
| 		Path: path.Join(workspace, "scripts", "unit-name"), | ||||
| 		Path:               path.Join("scripts", "unit-name"), | ||||
| 		RawFilePermissions: "0644", | ||||
| 		Content: name, | ||||
| 		Content:            name, | ||||
| 	} | ||||
| 	return system.WriteFile(&file) | ||||
| 	_, err := system.WriteFile(&file, workspace) | ||||
| 	return err | ||||
| } | ||||
|   | ||||
| @@ -31,33 +31,55 @@ func (f *File) Permissions() (os.FileMode, error) { | ||||
| 	return os.FileMode(perm), nil | ||||
| } | ||||
|  | ||||
| func WriteFile(f *File) error { | ||||
| func WriteFile(f *File, root string) (string, error) { | ||||
| 	if f.Encoding != "" { | ||||
| 		return fmt.Errorf("Unable to write file with encoding %s", 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 | ||||
| 	fullpath := path.Join(root, f.Path) | ||||
| 	dir := path.Dir(fullpath) | ||||
|  | ||||
| 	if err := EnsureDirectoryExists(dir); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	perm, err := f.Permissions() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if err := ioutil.WriteFile(f.Path, []byte(f.Content), perm); err != nil { | ||||
| 		return err | ||||
| 	var tmp *os.File | ||||
| 	// Create a temporary file in the same directory to ensure it's on the same filesystem | ||||
| 	if tmp, err = ioutil.TempFile(dir, "cloudinit-temp"); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if err := ioutil.WriteFile(tmp.Name(), []byte(f.Content), perm); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if err := tmp.Close(); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	// Ensure the permissions are as requested (since WriteFile can be affected by sticky bit) | ||||
| 	if err := os.Chmod(tmp.Name(), 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) | ||||
| 		cmd := exec.Command("chown", f.Owner, tmp.Name()) | ||||
| 		if err := cmd.Run(); err != nil { | ||||
| 			return err | ||||
| 			return "", err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| 	if err := os.Rename(tmp.Name(), fullpath); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return fullpath, nil | ||||
| } | ||||
|  | ||||
| func EnsureDirectoryExists(dir string) error { | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import ( | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"syscall" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| @@ -13,18 +12,22 @@ func TestWriteFileUnencodedContent(t *testing.T) { | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Unable to create tempdir: %v", err) | ||||
| 	} | ||||
| 	defer syscall.Rmdir(dir) | ||||
| 	defer os.RemoveAll(dir) | ||||
|  | ||||
| 	fullPath := path.Join(dir, "tmp", "foo") | ||||
| 	fn := "foo" | ||||
| 	fullPath := path.Join(dir, fn) | ||||
|  | ||||
| 	wf := File{ | ||||
| 		Path:        fullPath, | ||||
| 		Content:     "bar", | ||||
| 		Path:               fn, | ||||
| 		Content:            "bar", | ||||
| 		RawFilePermissions: "0644", | ||||
| 	} | ||||
|  | ||||
| 	if err := WriteFile(&wf); err != nil { | ||||
| 	path, err := WriteFile(&wf, dir) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Processing of WriteFile failed: %v", err) | ||||
| 	} else if path != fullPath { | ||||
| 		t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path) | ||||
| 	} | ||||
|  | ||||
| 	fi, err := os.Stat(fullPath) | ||||
| @@ -51,15 +54,15 @@ func TestWriteFileInvalidPermission(t *testing.T) { | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Unable to create tempdir: %v", err) | ||||
| 	} | ||||
| 	defer syscall.Rmdir(dir) | ||||
| 	defer os.RemoveAll(dir) | ||||
|  | ||||
| 	wf := File{ | ||||
| 		Path:        path.Join(dir, "tmp", "foo"), | ||||
| 		Content:     "bar", | ||||
| 		Path:               path.Join(dir, "tmp", "foo"), | ||||
| 		Content:            "bar", | ||||
| 		RawFilePermissions: "pants", | ||||
| 	} | ||||
|  | ||||
| 	if err := WriteFile(&wf); err == nil { | ||||
| 	if _, err := WriteFile(&wf, dir); err == nil { | ||||
| 		t.Fatalf("Expected error to be raised when writing file with invalid permission") | ||||
| 	} | ||||
| } | ||||
| @@ -69,17 +72,21 @@ func TestWriteFilePermissions(t *testing.T) { | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Unable to create tempdir: %v", err) | ||||
| 	} | ||||
| 	defer syscall.Rmdir(dir) | ||||
| 	defer os.RemoveAll(dir) | ||||
|  | ||||
| 	fullPath := path.Join(dir, "tmp", "foo") | ||||
| 	fn := "foo" | ||||
| 	fullPath := path.Join(dir, fn) | ||||
|  | ||||
| 	wf := File{ | ||||
| 		Path:               fullPath, | ||||
| 		Path:               fn, | ||||
| 		RawFilePermissions: "0755", | ||||
| 	} | ||||
|  | ||||
| 	if err := WriteFile(&wf); err != nil { | ||||
| 	path, err := WriteFile(&wf, dir) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Processing of WriteFile failed: %v", err) | ||||
| 	} else if path != fullPath { | ||||
| 		t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path) | ||||
| 	} | ||||
|  | ||||
| 	fi, err := os.Stat(fullPath) | ||||
| @@ -97,15 +104,15 @@ func TestWriteFileEncodedContent(t *testing.T) { | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Unable to create tempdir: %v", err) | ||||
| 	} | ||||
| 	defer syscall.Rmdir(dir) | ||||
| 	defer os.RemoveAll(dir) | ||||
|  | ||||
| 	wf := File{ | ||||
| 		Path: path.Join(dir, "tmp", "foo"), | ||||
| 		Content: "", | ||||
| 		Path:     path.Join(dir, "tmp", "foo"), | ||||
| 		Content:  "", | ||||
| 		Encoding: "base64", | ||||
| 	} | ||||
|  | ||||
| 	if err := WriteFile(&wf); err == nil { | ||||
| 	if _, err := WriteFile(&wf, dir); err == nil { | ||||
| 		t.Fatalf("Expected error to be raised when writing file with encoding") | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -78,12 +78,12 @@ func PlaceUnit(u *Unit, dst string) error { | ||||
| 	} | ||||
|  | ||||
| 	file := File{ | ||||
| 		Path:               dst, | ||||
| 		Path:               filepath.Base(dst), | ||||
| 		Content:            u.Content, | ||||
| 		RawFilePermissions: "0644", | ||||
| 	} | ||||
|  | ||||
| 	err := WriteFile(&file) | ||||
| 	_, err := WriteFile(&file, dir) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user