From 601350d14058d4cbcfa31261d90ac8d9e735365d Mon Sep 17 00:00:00 2001 From: Stepan Kokhanovskiy Date: Thu, 21 Sep 2023 15:11:27 +0300 Subject: [PATCH] Add base64 encoding support to file provider (#167) * Add base64 encoding support to file provider Signed-off-by: Stepan Kokhanovskiy * Fix git fatal error in forks Signed-off-by: Stepan Kokhanovskiy * Add tests for file provider Signed-off-by: Stepan Kokhanovskiy --------- Signed-off-by: Stepan Kokhanovskiy Co-authored-by: Stepan Kokhanovskiy --- Makefile | 2 +- README.md | 1 + pkg/providers/file/file.go | 28 ++++- pkg/providers/file/file_test.go | 208 ++++++++++++++++++++++++++++++++ 4 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 pkg/providers/file/file_test.go diff --git a/Makefile b/Makefile index 67bef95..401c8fb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -Version := $(shell git describe --tags --dirty) +Version := $(shell git describe --tags --dirty --always) GitCommit := $(shell git rev-parse HEAD) LDFLAGS := "-X main.version=$(Version) -X main.commit=$(GitCommit)" diff --git a/README.md b/README.md index 5f89f3a..e4b77f9 100644 --- a/README.md +++ b/README.md @@ -576,6 +576,7 @@ Examples: - `ref+file://foo/bar` loads the file at `foo/bar` - `ref+file:///home/foo/bar` loads the file at `/home/foo/bar` +- `ref+file://foo/bar?encode=base64` loads the file at `foo/bar` and encodes its content to a base64 string - `ref+file://some.yaml#/foo/bar` loads the YAML file at `some.yaml` and reads the value for the path `$.foo.bar`. Let's say `some.yaml` contains `{"foo":{"bar":"BAR"}}`, `key1: ref+file://some.yaml#/foo/bar` results in `key1: BAR`. diff --git a/pkg/providers/file/file.go b/pkg/providers/file/file.go index 85b3403..b2f2de0 100644 --- a/pkg/providers/file/file.go +++ b/pkg/providers/file/file.go @@ -1,6 +1,8 @@ package file import ( + "encoding/base64" + "fmt" "os" "strings" @@ -10,25 +12,45 @@ import ( ) type provider struct { + Encode string + fileReader func(string) ([]byte, error) } func New(cfg api.StaticConfig) *provider { p := &provider{} + p.fileReader = readFile + p.Encode = cfg.String("encode") + if p.Encode == "" { + p.Encode = "raw" + } return p } +func readFile(name string) ([]byte, error) { + return os.ReadFile(name) +} + func (p *provider) GetString(key string) (string, error) { + res := "" key = strings.TrimSuffix(key, "/") - bs, err := os.ReadFile(key) + bs, err := p.fileReader(key) if err != nil { return "", err } - return string(bs), nil + switch p.Encode { + case "raw": + res = string(bs) + case "base64": + res = base64.StdEncoding.EncodeToString(bs) + default: + return "", fmt.Errorf("Unsupported encode parameter: '%s'.", p.Encode) + } + return res, nil } func (p *provider) GetStringMap(key string) (map[string]interface{}, error) { key = strings.TrimSuffix(key, "/") - bs, err := os.ReadFile(key) + bs, err := p.fileReader(key) if err != nil { return nil, err } diff --git a/pkg/providers/file/file_test.go b/pkg/providers/file/file_test.go new file mode 100644 index 0000000..93036de --- /dev/null +++ b/pkg/providers/file/file_test.go @@ -0,0 +1,208 @@ +package file + +import ( + "encoding/base64" + "errors" + "fmt" + "testing" + + "github.com/helmfile/vals/pkg/config" +) + +const textFileContent = "file content" + +const yamlFileContent = ` +--- +foo: + bar: baz +` + +// Mock implementation of os.ReadFile for testing +func mockReadFile(name string) ([]byte, error) { + switch name { + case "path/to/file.txt": + return []byte(textFileContent), nil + case "path/to/empty_file.txt": + return []byte{}, nil + case "path/to/file.yaml": + return []byte(yamlFileContent), nil + case "path/to/error_file.txt": + return nil, errors.New("error reading file") + default: + return nil, errors.New("file not found") + } +} + +func Test_provider_GetString(t *testing.T) { + type params struct { + Encode string + } + type args struct { + key string + } + tests := []struct { + name string + params params + args args + want string + wantErr bool + }{ + { + name: "Encode parameter is empty", + params: params{ + Encode: "", + }, + args: args{ + key: "path/to/file.txt", + }, + want: textFileContent, + wantErr: false, + }, + { + name: "Encode parameter is 'raw'", + params: params{ + Encode: "raw", + }, + args: args{ + key: "path/to/file.txt", + }, + want: textFileContent, + wantErr: false, + }, + { + name: "Encode parameter is 'base64'", + params: params{ + Encode: "base64", + }, + args: args{ + key: "path/to/file.txt", + }, + want: base64.StdEncoding.EncodeToString([]byte(textFileContent)), + wantErr: false, + }, + { + name: "File is empty", + params: params{ + Encode: "raw", + }, + args: args{ + key: "path/to/empty_file.txt", + }, + want: "", + wantErr: false, + }, + { + name: "Error reading file", + params: params{ + Encode: "raw", + }, + args: args{ + key: "path/to/error_file.txt", + }, + want: "", + wantErr: true, + }, + { + name: "File not found", + params: params{ + Encode: "raw", + }, + args: args{ + key: "path/to/nonexistent_file.txt", + }, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create provider with mock + conf := map[string]interface{}{} + conf["encode"] = tt.params.Encode + p := New(config.MapConfig{M: conf}) + p.fileReader = mockReadFile + + got, err := p.GetString(tt.args.key) + if (err != nil) != tt.wantErr { + t.Errorf("provider.GetString() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("provider.GetString() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_provider_GetStringMap(t *testing.T) { + type args struct { + key string + } + tests := []struct { + name string + args args + want map[string]interface{} + wantErr bool + }{ + { + name: "Unmarshal valid yaml file", + args: args{ + key: "path/to/file.yaml", + }, + want: map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": "baz", + }, + }, + wantErr: false, + }, + { + name: "Unmarshal invalid yaml file", + args: args{ + key: "path/to/file.txt", + }, + want: nil, + wantErr: true, + }, + { + name: "File is empty", + args: args{ + key: "path/to/empty_file.txt", + }, + want: map[string]interface{}{}, + wantErr: false, + }, + { + name: "Error reading file", + args: args{ + key: "path/to/error_file.txt", + }, + want: nil, + wantErr: true, + }, + { + name: "File not found", + args: args{ + key: "path/to/nonexistent_file.txt", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf := map[string]interface{}{} + p := New(config.MapConfig{M: conf}) + p.fileReader = mockReadFile + + got, err := p.GetStringMap(tt.args.key) + if (err != nil) != tt.wantErr { + t.Errorf("provider.GetStringMap() error = %v, wantErr %v", err, tt.wantErr) + return + } + if fmt.Sprint(got) != fmt.Sprint(tt.want) { + t.Errorf("provider.GetStringMap() = %v, want %v", got, tt.want) + } + }) + } +}