file handler: Add "mkdirAll" flag

Signed-off-by: Malte Poll <mp@edgeless.systems>
This commit is contained in:
Malte Poll 2022-04-13 09:15:27 +02:00 committed by Malte Poll
parent 49a1a07049
commit e10a47f255
13 changed files with 66 additions and 38 deletions

View File

@ -112,7 +112,7 @@ func createAWS(cmd *cobra.Command, cl ec2client, fileHandler file.Handler, confi
if err != nil { if err != nil {
return err return err
} }
if err := fileHandler.WriteJSON(*config.StatePath, stat, false); err != nil { if err := fileHandler.WriteJSON(*config.StatePath, stat, file.OptNone); err != nil {
return err return err
} }

View File

@ -138,7 +138,7 @@ func TestCreateAWS(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs) fileHandler := file.NewHandler(fs)
if tc.existingState != nil { if tc.existingState != nil {
require.NoError(fileHandler.WriteJSON(*config.StatePath, *tc.existingState, false)) require.NoError(fileHandler.WriteJSON(*config.StatePath, *tc.existingState, file.OptNone))
} }
err := createAWS(cmd, tc.client, fileHandler, config, "xlarge", "name", 3) err := createAWS(cmd, tc.client, fileHandler, config, "xlarge", "name", 3)

View File

@ -120,7 +120,7 @@ func createAzure(cmd *cobra.Command, cl azureclient, fileHandler file.Handler, c
if err != nil { if err != nil {
return err return err
} }
if err := fileHandler.WriteJSON(*config.StatePath, stat, false); err != nil { if err := fileHandler.WriteJSON(*config.StatePath, stat, file.OptNone); err != nil {
return err return err
} }

View File

@ -155,7 +155,7 @@ func TestCreateAzure(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs) fileHandler := file.NewHandler(fs)
if tc.existingState != nil { if tc.existingState != nil {
require.NoError(fileHandler.WriteJSON(*config.StatePath, *tc.existingState, false)) require.NoError(fileHandler.WriteJSON(*config.StatePath, *tc.existingState, file.OptNone))
} }
err := createAzure(cmd, tc.client, fileHandler, config, "Standard_D2s_v3", 3, 2) err := createAzure(cmd, tc.client, fileHandler, config, "Standard_D2s_v3", 3, 2)

View File

@ -114,7 +114,7 @@ func createGCP(cmd *cobra.Command, cl gcpclient, fileHandler file.Handler, confi
return err return err
} }
if err := fileHandler.WriteJSON(*config.StatePath, stat, false); err != nil { if err := fileHandler.WriteJSON(*config.StatePath, stat, file.OptNone); err != nil {
return err return err
} }

View File

@ -153,7 +153,7 @@ func TestCreateGCP(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs) fileHandler := file.NewHandler(fs)
if tc.existingState != nil { if tc.existingState != nil {
require.NoError(fileHandler.WriteJSON(*config.StatePath, *tc.existingState, false)) require.NoError(fileHandler.WriteJSON(*config.StatePath, *tc.existingState, file.OptNone))
} }
err := createGCP(cmd, tc.client, fileHandler, config, "n2d-standard-2", 3, 2) err := createGCP(cmd, tc.client, fileHandler, config, "n2d-standard-2", 3, 2)

View File

@ -49,7 +49,7 @@ func TestCheckDirClean(t *testing.T) {
require := require.New(t) require := require.New(t)
for _, f := range tc.existingFiles { for _, f := range tc.existingFiles {
require.NoError(tc.fileHandler.Write(f, []byte{1, 2, 3}, false)) require.NoError(tc.fileHandler.Write(f, []byte{1, 2, 3}, file.OptNone))
} }
err := checkDirClean(tc.fileHandler, config) err := checkDirClean(tc.fileHandler, config)

View File

@ -101,7 +101,7 @@ func initialize(ctx context.Context, cmd *cobra.Command, protCl protoClient, ser
if err != nil { if err != nil {
return err return err
} }
if err := fileHandler.WriteJSON(*config.StatePath, stat, true); err != nil { if err := fileHandler.WriteJSON(*config.StatePath, stat, file.OptOverwrite); err != nil {
return err return err
} }
@ -228,7 +228,7 @@ func writeWGQuickFile(fileHandler file.Handler, config *config.Config, vpnHandle
if err != nil { if err != nil {
return err return err
} }
return fileHandler.Write(*config.WGQuickConfigPath, data, false) return fileHandler.Write(*config.WGQuickConfigPath, data, file.OptNone)
} }
func (r activationResult) writeOutput(wr io.Writer, fileHandler file.Handler, config *config.Config) error { func (r activationResult) writeOutput(wr io.Writer, fileHandler file.Handler, config *config.Config) error {
@ -245,7 +245,7 @@ func (r activationResult) writeOutput(wr io.Writer, fileHandler file.Handler, co
tw.Flush() tw.Flush()
fmt.Fprintln(wr) fmt.Fprintln(wr)
if err := fileHandler.Write(*config.AdminConfPath, []byte(r.kubeconfig), false); err != nil { if err := fileHandler.Write(*config.AdminConfPath, []byte(r.kubeconfig), file.OptNone); err != nil {
return fmt.Errorf("write kubeconfig: %w", err) return fmt.Errorf("write kubeconfig: %w", err)
} }
@ -360,7 +360,7 @@ func readOrGeneratedMasterSecret(w io.Writer, fileHandler file.Handler, filename
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := fileHandler.Write(*config.MasterSecretPath, []byte(base64.StdEncoding.EncodeToString(masterSecret)), false); err != nil { if err := fileHandler.Write(*config.MasterSecretPath, []byte(base64.StdEncoding.EncodeToString(masterSecret)), file.OptNone); err != nil {
return nil, err return nil, err
} }
fmt.Fprintf(w, "Your Constellation master secret was successfully written to ./%s\n", *config.MasterSecretPath) fmt.Fprintf(w, "Your Constellation master secret was successfully written to ./%s\n", *config.MasterSecretPath)

View File

@ -336,7 +336,7 @@ func TestInitialize(t *testing.T) {
cmd.SetErr(&errOut) cmd.SetErr(&errOut)
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs) fileHandler := file.NewHandler(fs)
require.NoError(fileHandler.WriteJSON(*config.StatePath, tc.existingState, false)) require.NoError(fileHandler.WriteJSON(*config.StatePath, tc.existingState, file.OptNone))
// Write key file to filesystem and set path in flag. // Write key file to filesystem and set path in flag.
require.NoError(afero.Afero{Fs: fs}.WriteFile("privK", []byte(tc.privKey), 0o600)) require.NoError(afero.Afero{Fs: fs}.WriteFile("privK", []byte(tc.privKey), 0o600))
@ -444,7 +444,7 @@ func TestReadOrGenerateVPNKey(t *testing.T) {
testKey := []byte(base64.StdEncoding.EncodeToString([]byte("32bytesWireGuardKeyForTheTesting"))) testKey := []byte(base64.StdEncoding.EncodeToString([]byte("32bytesWireGuardKeyForTheTesting")))
fileHandler := file.NewHandler(afero.NewMemMapFs()) fileHandler := file.NewHandler(afero.NewMemMapFs())
require.NoError(fileHandler.Write("testKey", testKey, false)) require.NoError(fileHandler.Write("testKey", testKey, file.OptNone))
privK, pubK, err := readOrGenerateVPNKey(fileHandler, "testKey") privK, pubK, err := readOrGenerateVPNKey(fileHandler, "testKey")
assert.NoError(err) assert.NoError(err)
@ -525,7 +525,7 @@ func TestReadOrGeneratedMasterSecret(t *testing.T) {
config := config.Default() config := config.Default()
if tc.createFile { if tc.createFile {
require.NoError(fileHandler.Write(tc.filename, []byte(tc.filecontent), false)) require.NoError(fileHandler.Write(tc.filename, []byte(tc.filecontent), file.OptNone))
} }
var out bytes.Buffer var out bytes.Buffer
@ -697,7 +697,7 @@ func TestAutoscaleFlag(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs) fileHandler := file.NewHandler(fs)
vpnHandler := stubVPNHandler{} vpnHandler := stubVPNHandler{}
require.NoError(fileHandler.WriteJSON(*config.StatePath, tc.existingState, false)) require.NoError(fileHandler.WriteJSON(*config.StatePath, tc.existingState, file.OptNone))
// Write key file to filesystem and set path in flag. // Write key file to filesystem and set path in flag.
require.NoError(afero.Afero{Fs: fs}.WriteFile("privK", []byte(tc.privKey), 0o600)) require.NoError(afero.Afero{Fs: fs}.WriteFile("privK", []byte(tc.privKey), 0o600))

View File

@ -9,10 +9,28 @@ import (
"io" "io"
"io/fs" "io/fs"
"os" "os"
"path"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
// Option is a bitmask of options for file operations.
type Option uint
// Has determines if a set of options contains the given options.
func (o Option) Has(op Option) bool {
return o&op == op
}
const (
// OptNone is a no-op.
OptNone Option = 1 << iota / 2
// OptOverwrite overwrites an existing file.
OptOverwrite
// OptMkdirAll creates the path to the file.
OptMkdirAll
)
// Handler handles file interaction. // Handler handles file interaction.
type Handler struct { type Handler struct {
fs *afero.Afero fs *afero.Afero
@ -35,11 +53,14 @@ func (h *Handler) Read(name string) ([]byte, error) {
} }
// Write writes the data bytes into the file with the given name. // Write writes the data bytes into the file with the given name.
// If a file already exists at path and overwrite is true, the file will be func (h *Handler) Write(name string, data []byte, options Option) error {
// overwritten. Otherwise, an error is returned. if options.Has(OptMkdirAll) {
func (h *Handler) Write(name string, data []byte, overwrite bool) error { if err := h.fs.MkdirAll(path.Dir(name), os.ModePerm); err != nil {
return err
}
}
flags := os.O_WRONLY | os.O_CREATE | os.O_EXCL flags := os.O_WRONLY | os.O_CREATE | os.O_EXCL
if overwrite { if options.Has(OptOverwrite) {
flags = os.O_WRONLY | os.O_CREATE | os.O_TRUNC flags = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
} }
file, err := h.fs.OpenFile(name, flags, 0o644) file, err := h.fs.OpenFile(name, flags, 0o644)
@ -64,14 +85,12 @@ func (h *Handler) ReadJSON(name string, content interface{}) error {
} }
// WriteJSON marshals the content interface to JSON and writes it to the path with the given name. // WriteJSON marshals the content interface to JSON and writes it to the path with the given name.
// If a file already exists and overwrite is true, the file will be func (h *Handler) WriteJSON(name string, content interface{}, options Option) error {
// overwritten. Otherwise, an error is returned.
func (h *Handler) WriteJSON(name string, content interface{}, overwrite bool) error {
jsonData, err := json.MarshalIndent(content, "", "\t") jsonData, err := json.MarshalIndent(content, "", "\t")
if err != nil { if err != nil {
return err return err
} }
return h.Write(name, jsonData, overwrite) return h.Write(name, jsonData, options)
} }
// Remove deletes the file with the given name. // Remove deletes the file with the given name.

View File

@ -80,12 +80,12 @@ func TestWriteJSON(t *testing.T) {
notMarshalableContent := struct{ Foo chan int }{Foo: make(chan int)} notMarshalableContent := struct{ Foo chan int }{Foo: make(chan int)}
testCases := map[string]struct { testCases := map[string]struct {
fs afero.Fs fs afero.Fs
setupFs func(af afero.Afero) error setupFs func(af afero.Afero) error
name string name string
content interface{} content interface{}
overwrite bool options Option
wantErr bool wantErr bool
}{ }{
"successful write": { "successful write": {
fs: afero.NewMemMapFs(), fs: afero.NewMemMapFs(),
@ -93,11 +93,11 @@ func TestWriteJSON(t *testing.T) {
content: someContent, content: someContent,
}, },
"successful overwrite": { "successful overwrite": {
fs: afero.NewMemMapFs(), fs: afero.NewMemMapFs(),
setupFs: func(af afero.Afero) error { return af.WriteFile("test/statefile", []byte{}, 0o644) }, setupFs: func(af afero.Afero) error { return af.WriteFile("test/statefile", []byte{}, 0o644) },
name: "test/statefile", name: "test/statefile",
content: someContent, content: someContent,
overwrite: true, options: OptOverwrite,
}, },
"read only fs": { "read only fs": {
fs: afero.NewReadOnlyFs(afero.NewMemMapFs()), fs: afero.NewReadOnlyFs(afero.NewMemMapFs()),
@ -118,6 +118,15 @@ func TestWriteJSON(t *testing.T) {
content: notMarshalableContent, content: notMarshalableContent,
wantErr: true, wantErr: true,
}, },
"mkdirAll works": {
fs: afero.NewMemMapFs(),
name: "test/statefile",
content: someContent,
options: OptMkdirAll,
},
// TODO: add tests for mkdirAll actually creating the necessary folders when https://github.com/spf13/afero/issues/270 is fixed.
// Currently, MemMapFs will create files in nonexistent directories due to a bug in afero,
// making it impossible to test the actual behavior of the mkdirAll parameter.
} }
for name, tc := range testCases { for name, tc := range testCases {
@ -131,9 +140,9 @@ func TestWriteJSON(t *testing.T) {
} }
if tc.wantErr { if tc.wantErr {
assert.Error(handler.WriteJSON(tc.name, tc.content, tc.overwrite)) assert.Error(handler.WriteJSON(tc.name, tc.content, tc.options))
} else { } else {
assert.NoError(handler.WriteJSON(tc.name, tc.content, tc.overwrite)) assert.NoError(handler.WriteJSON(tc.name, tc.content, tc.options))
resultContent := &testContent{} resultContent := &testContent{}
assert.NoError(handler.ReadJSON(tc.name, resultContent)) assert.NoError(handler.ReadJSON(tc.name, resultContent))
assert.Equal(tc.content, *resultContent) assert.Equal(tc.content, *resultContent)

View File

@ -27,5 +27,5 @@ func FromFile(fileHandler file.Handler) (*NodeState, error) {
// ToFile writes a NodeState to disk. // ToFile writes a NodeState to disk.
func (nodeState *NodeState) ToFile(fileHandler file.Handler) error { func (nodeState *NodeState) ToFile(fileHandler file.Handler) error {
return fileHandler.WriteJSON(nodeStatePath, nodeState, false) return fileHandler.WriteJSON(nodeStatePath, nodeState, false, true)
} }

View File

@ -77,7 +77,7 @@ func TestFromFile(t *testing.T) {
require := require.New(t) require := require.New(t)
fileHandler := file.NewHandler(afero.NewMemMapFs()) fileHandler := file.NewHandler(afero.NewMemMapFs())
require.NoError(fileHandler.WriteJSON(configName, tc.from, false)) require.NoError(fileHandler.WriteJSON(configName, tc.from, file.OptNone))
result, err := FromFile(fileHandler, tc.configName) result, err := FromFile(fileHandler, tc.configName)