Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add diskutil package for Atomic File Writing Operations #187

Merged
merged 2 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions pkg/common/diskutil/disk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package diskutil

import (
"os"
"path/filepath"
)

// Define the file mode for private files.
const (
fileModePrivate = 0600
)

// AtomicWritePrivateFile writes data to a file atomically.
// The file is created with private permissions (0600).
func AtomicWritePrivateFile(path string, data []byte) error {
return atomicWrite(path, data, fileModePrivate)
}

func atomicWrite(path string, data []byte, mode os.FileMode) error {
tmpPath := path + ".tmp"

// Attempt to write to temporary file
if err := write(tmpPath, data, mode); err != nil {
return err
}

// If write is successful, rename temp file to actual path
return rename(tmpPath, path)
}

func write(path string, data []byte, mode os.FileMode) error {
// Attempt to open file, deferring close until function finishes
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
if err != nil {
return err
}
defer file.Close()

_, err = file.Write(data)
if err != nil {
return err
}

// Sync file contents to disk
return file.Sync()
}

func rename(oldPath, newPath string) error {
// Attempt to rename file
if err := os.Rename(oldPath, newPath); err != nil {
return err
}

// Open containing directory and defer close until function finishes
dir, err := os.Open(filepath.Dir(newPath))
if err != nil {
return err
}
defer dir.Close()

// Sync directory changes to disk
return dir.Sync()
}
61 changes: 61 additions & 0 deletions pkg/common/diskutil/disk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package diskutil

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
)

func TestWriteFile(t *testing.T) {
tempDir, err := os.MkdirTemp("", "galadriel-test")
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, os.RemoveAll(tempDir))
})

tests := []struct {
name string
data []byte
atomicWriteFunc func(string, []byte) error
expectMode os.FileMode
}{
{
name: "basic - AtomicWritePrivateFile",
data: []byte("Hello, World"),
atomicWriteFunc: AtomicWritePrivateFile,
expectMode: 0600,
},
{
name: "empty - AtomicWritePrivateFile",
data: []byte{},
atomicWriteFunc: AtomicWritePrivateFile,
expectMode: 0600,
},
{
name: "binary - AtomicWritePrivateFile",
data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9},
atomicWriteFunc: AtomicWritePrivateFile,
expectMode: 0600,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
file := filepath.Join(tempDir, "file")
err := tt.atomicWriteFunc(file, tt.data)
require.NoError(t, err)

info, err := os.Stat(file)
require.NoError(t, err)
require.EqualValues(t, tt.expectMode, info.Mode())

content, err := os.ReadFile(file)
require.NoError(t, err)
require.Equal(t, tt.data, content)

require.NoError(t, os.Remove(file))
})
}
}
5 changes: 2 additions & 3 deletions pkg/common/keymanager/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ import (
"os"

"github.com/HewlettPackard/galadriel/pkg/common/cryptoutil"
"github.com/HewlettPackard/galadriel/pkg/common/diskutil"
)

const keyFilePerm = 0600

// Disk extends the base KeyManager to store keys in disk.
type Disk struct {
base
Expand Down Expand Up @@ -114,7 +113,7 @@ func (d *Disk) saveKeysToDisk() error {
return fmt.Errorf("failed to serialize keys: %w", err)
}

if err := os.WriteFile(d.keysFilePath, data, keyFilePerm); err != nil {
if err := diskutil.AtomicWritePrivateFile(d.keysFilePath, data); err != nil {
return fmt.Errorf("failed to write keys to disk: %w", err)
}

Expand Down
7 changes: 3 additions & 4 deletions pkg/harvester/galadrielclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/HewlettPackard/galadriel/pkg/common/api"
"github.com/HewlettPackard/galadriel/pkg/common/constants"
"github.com/HewlettPackard/galadriel/pkg/common/diskutil"
"github.com/HewlettPackard/galadriel/pkg/common/entity"
"github.com/HewlettPackard/galadriel/pkg/common/util"
"github.com/HewlettPackard/galadriel/pkg/server/api/harvester"
Expand All @@ -30,7 +31,6 @@ const (
jwtRotationInterval = 5 * time.Minute
onboardPath = "/trust-domain/onboard"
tokenFile = "jwt-token"
tokenFileMode = 0600
)

var (
Expand Down Expand Up @@ -547,8 +547,7 @@ func (p *jwtStore) setToken(jwt string) {
p.mu.Unlock()

// Save the token to disk
err := p.saveToken()
if err != nil {
if err := p.saveToken(); err != nil {
p.logger.Errorf("Failed to save JWT token to disk: %v", err)
}
}
Expand Down Expand Up @@ -581,5 +580,5 @@ func (p *jwtStore) saveToken() error {
p.mu.RLock()
defer p.mu.RUnlock()

return os.WriteFile(p.tokenFilePath, []byte(p.jwt), tokenFileMode)
return diskutil.AtomicWritePrivateFile(p.tokenFilePath, []byte(p.jwt))
}