From 7f588375703077113eae586e85ea321baa3d040c Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Mon, 31 Mar 2025 15:42:43 +0900 Subject: [PATCH 1/4] Support file/format for kv_store and secret_store --- CHANGELOG.md | 2 + pkg/manifest/file.go | 102 ++++++++ pkg/manifest/local_server.go | 180 ++++++++++++- pkg/manifest/local_server_test.go | 246 ++++++++++++++++++ .../testdata/fastly-viceroy-update.toml | 2 + 5 files changed, 523 insertions(+), 9 deletions(-) create mode 100644 pkg/manifest/local_server_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c9a6502..aefa65104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Enhancements: +- feat(config): Support file/format for kv_store and secret_store in fastly.toml + ### Bug fixes: ### Dependencies: diff --git a/pkg/manifest/file.go b/pkg/manifest/file.go index 7ba4826e9..220c2d5b9 100644 --- a/pkg/manifest/file.go +++ b/pkg/manifest/file.go @@ -2,6 +2,7 @@ package manifest import ( "bufio" + "bytes" "fmt" "io" "os" @@ -50,6 +51,107 @@ type File struct { readError error } +// MarshalTOML performs custom marshalling to TOML for objects of File type. +func (f *File) MarshalTOML() ([]byte, error) { + localServer := make(map[string]interface{}) + + if f.LocalServer.Backends != nil { + localServer["backends"] = f.LocalServer.Backends + } + + if f.LocalServer.ConfigStores != nil { + localServer["config_stores"] = f.LocalServer.ConfigStores + } + + if f.LocalServer.KVStores != nil { + kvStores := make(map[string]interface{}) + for key, entry := range f.LocalServer.KVStores { + if entry.External != nil { + kvStores[key] = map[string]interface{}{ + "file": entry.External.File, + "format": entry.External.Format, + } + } else { + items := make([]map[string]interface{}, 0, len(entry.Array)) + for _, e := range entry.Array { + obj := map[string]interface{}{"key": e.Key} + if e.File != "" { + obj["file"] = e.File + } + if e.Data != "" { + obj["data"] = e.Data + } + items = append(items, obj) + } + kvStores[key] = items + } + } + localServer["kv_stores"] = kvStores + } + + if f.LocalServer.SecretStores != nil { + secretStores := make(map[string]interface{}) + for key, entry := range f.LocalServer.SecretStores { + if entry.External != nil { + secretStores[key] = map[string]interface{}{ + "file": entry.External.File, + "format": entry.External.Format, + } + } else { + items := make([]map[string]interface{}, 0, len(entry.Array)) + for _, e := range entry.Array { + obj := map[string]interface{}{"key": e.Key} + if e.File != "" { + obj["file"] = e.File + } + if e.Data != "" { + obj["data"] = e.Data + } + items = append(items, obj) + } + secretStores[key] = items + } + } + localServer["secret_stores"] = secretStores + } + + if f.LocalServer.ViceroyVersion != "" { + localServer["viceroy_version"] = f.LocalServer.ViceroyVersion + } + + type outputFile struct { + Authors []string `toml:"authors"` + ClonedFrom string `toml:"cloned_from,omitempty"` + Description string `toml:"description"` + Language string `toml:"language"` + Profile string `toml:"profile,omitempty"` + LocalServer interface{} `toml:"local_server"` // override this field + ManifestVersion Version `toml:"manifest_version"` + Name string `toml:"name"` + Scripts Scripts `toml:"scripts,omitempty"` + ServiceID string `toml:"service_id"` + Setup Setup `toml:"setup,omitempty"` + } + + out := outputFile{ + Authors: f.Authors, + ClonedFrom: f.ClonedFrom, + Description: f.Description, + Language: f.Language, + Profile: f.Profile, + LocalServer: localServer, + ManifestVersion: f.ManifestVersion, + Name: f.Name, + Scripts: f.Scripts, + ServiceID: f.ServiceID, + Setup: f.Setup, + } + + var buf bytes.Buffer + err := toml.NewEncoder(&buf).Encode(out) + return buf.Bytes(), err +} + // Exists yields whether the manifest exists. // // Specifically, it indicates that a toml.Unmarshal() of the toml disk content diff --git a/pkg/manifest/local_server.go b/pkg/manifest/local_server.go index eb607af58..59bbeed98 100644 --- a/pkg/manifest/local_server.go +++ b/pkg/manifest/local_server.go @@ -1,12 +1,19 @@ package manifest +import ( + "bytes" + "fmt" + + "github.com/pelletier/go-toml" +) + // LocalServer represents a list of mocked Viceroy resources. type LocalServer struct { - Backends map[string]LocalBackend `toml:"backends"` - ConfigStores map[string]LocalConfigStore `toml:"config_stores,omitempty"` - KVStores map[string][]LocalKVStore `toml:"kv_stores,omitempty"` - SecretStores map[string][]LocalSecretStore `toml:"secret_stores,omitempty"` - ViceroyVersion string `toml:"viceroy_version,omitempty"` + Backends map[string]LocalBackend `toml:"backends"` + ConfigStores map[string]LocalConfigStore `toml:"config_stores,omitempty"` + KVStores LocalKVStoreMap `toml:"kv_stores,omitempty"` + SecretStores LocalSecretStoreMap `toml:"secret_stores,omitempty"` + ViceroyVersion string `toml:"viceroy_version,omitempty"` } // LocalBackend represents a backend to be mocked by the local testing server. @@ -24,16 +31,171 @@ type LocalConfigStore struct { Contents map[string]string `toml:"contents,omitempty"` } -// LocalKVStore represents an kv_store to be mocked by the local testing server. -type LocalKVStore struct { +// KVStoreArrayEntry represents an array-based key/value store entries. +// It expects a key plus either a data or file field. +type KVStoreArrayEntry struct { Key string `toml:"key"` File string `toml:"file,omitempty"` Data string `toml:"data,omitempty"` } -// LocalSecretStore represents a secret_store to be mocked by the local testing server. -type LocalSecretStore struct { +// KVStoreExternalFile represents the external key/value store, +// which must have both a file and a format. +type KVStoreExternalFile struct { + File string `toml:"file"` + Format string `toml:"format"` +} + +// LocalKVStore represents a kv_store to be mocked by the local testing server. +// It is a union type and can either be an array of KVStoreArrayEntry or a single KVStoreExternalFile. +// The IsArray flag is used to preserve the original input style. +type LocalKVStore struct { + IsArray bool `toml:"-"` + Array []KVStoreArrayEntry `toml:"-"` + External *KVStoreExternalFile `toml:"-"` +} + +// LocalKVStoreMap is a map of kv_store names to the local kv_store representation. +type LocalKVStoreMap map[string]LocalKVStore + +// UnmarshalTOML performs custom unmarshalling of TOML data for LocalKVStoreMap. +func (m *LocalKVStoreMap) UnmarshalTOML(v interface{}) error { + raw, ok := v.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected kv_stores to be a TOML table") + } + + result := make(LocalKVStoreMap) + + for key, val := range raw { + switch typed := val.(type) { + case []interface{}: + var entries []KVStoreArrayEntry + for _, item := range typed { + obj, ok := item.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid item in array for key %q", key) + } + var arrayEntry KVStoreArrayEntry + if err := decodeTOMLMap(obj, &arrayEntry); err != nil { + return fmt.Errorf("decode failed for array item in key %q: %w", key, err) + } + entries = append(entries, arrayEntry) + } + result[key] = LocalKVStore{ + IsArray: true, + Array: entries, + } + + case map[string]interface{}: + file, hasFile := typed["file"].(string) + format, hasFormat := typed["format"].(string) + + if !hasFile || !hasFormat { + return fmt.Errorf("key %q must have both file and format", key) + } + result[key] = LocalKVStore{ + IsArray: false, + External: &KVStoreExternalFile{ + File: file, + Format: format, + }, + } + + default: + return fmt.Errorf("unsupported value type for key %q: %T", key, typed) + } + } + + *m = result + return nil +} + +// SecretStoreArrayEntry represents an array-based key/value store entries. +// It expects a key plus either a data or file field. +type SecretStoreArrayEntry struct { Key string `toml:"key"` File string `toml:"file,omitempty"` Data string `toml:"data,omitempty"` } + +// SecretStoreExternalFile represents the external key/value store, +// which must have both a file and a format. +type SecretStoreExternalFile struct { + File string `toml:"file"` + Format string `toml:"format"` +} + +// LocalSecretStore represents a secret_store to be mocked by the local testing server. +// It is a union type and can either be an array of SecretStoreArrayEntry or a single SecretStoreExternalFile. +// The IsArray flag is used to preserve the original input style. +type LocalSecretStore struct { + IsArray bool `toml:"-"` + Array []SecretStoreArrayEntry `toml:"-"` + External *SecretStoreExternalFile `toml:"-"` +} + +// LocalSecretStoreMap is a map of secret_store names to the local secret_store representation. +type LocalSecretStoreMap map[string]LocalSecretStore + +// UnmarshalTOML performs custom unmarshalling of TOML data for LocalSecretStoreMap. +func (m *LocalSecretStoreMap) UnmarshalTOML(v interface{}) error { + raw, ok := v.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected secret_stores to be a TOML table") + } + + result := make(LocalSecretStoreMap) + + for key, val := range raw { + switch typed := val.(type) { + case []interface{}: + var entries []SecretStoreArrayEntry + for _, item := range typed { + obj, ok := item.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid item in array for key %q", key) + } + var arrayEntry SecretStoreArrayEntry + if err := decodeTOMLMap(obj, &arrayEntry); err != nil { + return fmt.Errorf("decode failed for array item in key %q: %w", key, err) + } + entries = append(entries, arrayEntry) + } + result[key] = LocalSecretStore{ + IsArray: true, + Array: entries, + } + + case map[string]interface{}: + file, hasFile := typed["file"].(string) + format, hasFormat := typed["format"].(string) + + if !hasFile || !hasFormat { + return fmt.Errorf("key %q must have both file and format", key) + } + result[key] = LocalSecretStore{ + IsArray: false, + External: &SecretStoreExternalFile{ + File: file, + Format: format, + }, + } + + default: + return fmt.Errorf("unsupported value type for key %q: %T", key, typed) + } + } + + *m = result + return nil +} + +func decodeTOMLMap(m map[string]interface{}, out interface{}) error { + buf := new(bytes.Buffer) + enc := toml.NewEncoder(buf) + if err := enc.Encode(m); err != nil { + return err + } + return toml.NewDecoder(buf).Decode(out) +} diff --git a/pkg/manifest/local_server_test.go b/pkg/manifest/local_server_test.go new file mode 100644 index 000000000..badcd5303 --- /dev/null +++ b/pkg/manifest/local_server_test.go @@ -0,0 +1,246 @@ +package manifest + +import ( + "bytes" + "log" + "strings" + "testing" + + "github.com/pelletier/go-toml" +) + +type KVWrapper struct { + KVStores LocalKVStoreMap `toml:"kv_stores"` +} + +func (w KVWrapper) MarshalTOML() ([]byte, error) { + obj := make(map[string]interface{}) + kv := make(map[string]interface{}) + + for key, entry := range w.KVStores { + if entry.External != nil { + kv[key] = map[string]interface{}{ + "file": entry.External.File, + "format": entry.External.Format, + } + } else { + kv[key] = entry.Array + } + } + + obj["kv_stores"] = kv + + buf := new(bytes.Buffer) + err := toml.NewEncoder(buf).Encode(obj) + return buf.Bytes(), err +} + +type SecretStoreWrapper struct { + SecretStores LocalSecretStoreMap `toml:"secret_stores"` +} + +func (w SecretStoreWrapper) MarshalTOML() ([]byte, error) { + obj := make(map[string]interface{}) + kv := make(map[string]interface{}) + + for key, entry := range w.SecretStores { + if entry.External != nil { + kv[key] = map[string]interface{}{ + "file": entry.External.File, + "format": entry.External.Format, + } + } else { + kv[key] = entry.Array + } + } + + obj["kv_stores"] = kv + + buf := new(bytes.Buffer) + err := toml.NewEncoder(buf).Encode(obj) + return buf.Bytes(), err +} + +func TestLocalKVStores_UnmarshalTOML(t *testing.T) { + tests := []struct { + name string + inputTOML string + expectError bool + expectArray bool + expectLength int + wantFile string + }{ + { + name: "legacy array format", + inputTOML: ` +[[kv_stores.my-kv]] +key = "my-kv" +file = "kv.json" +`, + expectArray: true, + expectLength: 1, + wantFile: "kv.json", + }, + { + name: "external file format", + inputTOML: ` +[kv_stores] +my-kv = { file = "kv.json", format = "json" } +`, + expectArray: false, + expectLength: 0, + wantFile: "kv.json", + }, + { + name: "invalid format", + inputTOML: ` +[kv_stores] +my-kv = "not-a-valid-entry" +`, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var m KVWrapper + + decoder := toml.NewDecoder(strings.NewReader(tt.inputTOML)) + err := decoder.Decode(&m) + + buf := new(bytes.Buffer) + encoder := toml.NewEncoder(buf) + + encodeErr := encoder.Encode(m) + if encodeErr != nil { + log.Fatalf("TOML encode failed: %v", encodeErr) + } + + if tt.expectError { + if err == nil { + t.Fatal("Expected error for invalid format, but got none") + } + return + } else if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + got, ok := m.KVStores["my-kv"] + if !ok { + t.Fatalf("Expected key 'my-kv' not found") + } + + if got.IsArray != tt.expectArray { + t.Fatalf("Expected IsArray=%v, got %v", tt.expectArray, got.IsArray) + } + + if tt.expectArray { + if len(got.Array) != tt.expectLength { + t.Fatalf("Expected %d inline entries, got %d", tt.expectLength, len(got.Array)) + } + if got.Array[0].File != tt.wantFile { + t.Errorf("Expected file %q, got %q", tt.wantFile, got.Array[0].File) + } + } else { + if got.External == nil { + t.Fatal("Expected KVStoreExternalFile but got nil") + } + if got.External.File != tt.wantFile { + t.Errorf("Expected file %q, got %q", tt.wantFile, got.External.File) + } + } + }) + } +} + +func TestLocalSecretStores_UnmarshalTOML(t *testing.T) { + tests := []struct { + name string + inputTOML string + expectError bool + expectArray bool + expectLength int + wantFile string + }{ + { + name: "legacy array format", + inputTOML: ` +[[secret_stores.my-secret-store]] +key = "secret" +file = "secret.json" +`, + expectArray: true, + expectLength: 1, + wantFile: "secret.json", + }, + { + name: "external file format", + inputTOML: ` +[secret_stores] +my-secret-store = { file = "kv.json", format = "json" } +`, + expectArray: false, + expectLength: 0, + wantFile: "kv.json", + }, + { + name: "invalid format", + inputTOML: ` +[secret_stores] +my-secret-store = "not-a-valid-entry" +`, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var m SecretStoreWrapper + + decoder := toml.NewDecoder(strings.NewReader(tt.inputTOML)) + err := decoder.Decode(&m) + + buf := new(bytes.Buffer) + encoder := toml.NewEncoder(buf) + + encodeErr := encoder.Encode(m) + if encodeErr != nil { + log.Fatalf("TOML encode failed: %v", encodeErr) + } + + if tt.expectError { + if err == nil { + t.Fatal("Expected error for invalid format, but got none") + } + return + } else if err != nil { + t.Fatalf("Failed to parse TOML: %v", err) + } + + got, ok := m.SecretStores["my-secret-store"] + if !ok { + t.Fatalf("Expected key 'my-secret-store' not found") + } + + if got.IsArray != tt.expectArray { + t.Fatalf("Expected IsArray=%v, got %v", tt.expectArray, got.IsArray) + } + + if tt.expectArray { + if len(got.Array) != tt.expectLength { + t.Fatalf("Expected %d inline entries, got %d", tt.expectLength, len(got.Array)) + } + if got.Array[0].File != tt.wantFile { + t.Errorf("Expected file %q, got %q", tt.wantFile, got.Array[0].File) + } + } else { + if got.External == nil { + t.Fatal("Expected SecretStoreExternalFile but got nil") + } + if got.External.File != tt.wantFile { + t.Errorf("Expected file %q, got %q", tt.wantFile, got.External.File) + } + } + }) + } +} diff --git a/pkg/manifest/testdata/fastly-viceroy-update.toml b/pkg/manifest/testdata/fastly-viceroy-update.toml index 2c869d21e..9434a538b 100644 --- a/pkg/manifest/testdata/fastly-viceroy-update.toml +++ b/pkg/manifest/testdata/fastly-viceroy-update.toml @@ -40,6 +40,7 @@ store_one = [ { key = "first", data = "This is some data" }, { key = "second", file = "strings.json" }, ] +store_three = { file = "path/to/kv.json", format = "json" } [[local_server.kv_stores.store_two]] key = "first" @@ -54,6 +55,7 @@ store_one = [ { key = "first", data = "This is some secret data" }, { key = "second", file = "/path/to/secret.json" }, ] +store_three = { file = "path/to/secret.json", format = "json" } [[local_server.secret_stores.store_two]] key = "first" From 268cd628e720a7531bac7558fb1c89eb74c840bf Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Wed, 2 Apr 2025 15:21:52 +0900 Subject: [PATCH 2/4] Apply suggestions from PR --- pkg/manifest/file.go | 46 +++++----- pkg/manifest/local_server.go | 22 ++--- pkg/manifest/local_server_test.go | 138 ++++++++++++------------------ 3 files changed, 86 insertions(+), 120 deletions(-) diff --git a/pkg/manifest/file.go b/pkg/manifest/file.go index 220c2d5b9..954de8696 100644 --- a/pkg/manifest/file.go +++ b/pkg/manifest/file.go @@ -53,7 +53,7 @@ type File struct { // MarshalTOML performs custom marshalling to TOML for objects of File type. func (f *File) MarshalTOML() ([]byte, error) { - localServer := make(map[string]interface{}) + localServer := make(map[string]any) if f.LocalServer.Backends != nil { localServer["backends"] = f.LocalServer.Backends @@ -64,17 +64,17 @@ func (f *File) MarshalTOML() ([]byte, error) { } if f.LocalServer.KVStores != nil { - kvStores := make(map[string]interface{}) + kvStores := make(map[string]any) for key, entry := range f.LocalServer.KVStores { if entry.External != nil { - kvStores[key] = map[string]interface{}{ + kvStores[key] = map[string]any{ "file": entry.External.File, "format": entry.External.Format, } } else { - items := make([]map[string]interface{}, 0, len(entry.Array)) + items := make([]map[string]any, 0, len(entry.Array)) for _, e := range entry.Array { - obj := map[string]interface{}{"key": e.Key} + obj := map[string]any{"key": e.Key} if e.File != "" { obj["file"] = e.File } @@ -90,17 +90,17 @@ func (f *File) MarshalTOML() ([]byte, error) { } if f.LocalServer.SecretStores != nil { - secretStores := make(map[string]interface{}) + secretStores := make(map[string]any) for key, entry := range f.LocalServer.SecretStores { if entry.External != nil { - secretStores[key] = map[string]interface{}{ + secretStores[key] = map[string]any{ "file": entry.External.File, "format": entry.External.Format, } } else { - items := make([]map[string]interface{}, 0, len(entry.Array)) + items := make([]map[string]any, 0, len(entry.Array)) for _, e := range entry.Array { - obj := map[string]interface{}{"key": e.Key} + obj := map[string]any{"key": e.Key} if e.File != "" { obj["file"] = e.File } @@ -119,21 +119,19 @@ func (f *File) MarshalTOML() ([]byte, error) { localServer["viceroy_version"] = f.LocalServer.ViceroyVersion } - type outputFile struct { - Authors []string `toml:"authors"` - ClonedFrom string `toml:"cloned_from,omitempty"` - Description string `toml:"description"` - Language string `toml:"language"` - Profile string `toml:"profile,omitempty"` - LocalServer interface{} `toml:"local_server"` // override this field - ManifestVersion Version `toml:"manifest_version"` - Name string `toml:"name"` - Scripts Scripts `toml:"scripts,omitempty"` - ServiceID string `toml:"service_id"` - Setup Setup `toml:"setup,omitempty"` - } - - out := outputFile{ + out := struct { + Authors []string `toml:"authors"` + ClonedFrom string `toml:"cloned_from,omitempty"` + Description string `toml:"description"` + Language string `toml:"language"` + Profile string `toml:"profile,omitempty"` + LocalServer any `toml:"local_server"` // override this field + ManifestVersion Version `toml:"manifest_version"` + Name string `toml:"name"` + Scripts Scripts `toml:"scripts,omitempty"` + ServiceID string `toml:"service_id"` + Setup Setup `toml:"setup,omitempty"` + }{ Authors: f.Authors, ClonedFrom: f.ClonedFrom, Description: f.Description, diff --git a/pkg/manifest/local_server.go b/pkg/manifest/local_server.go index 59bbeed98..b66632369 100644 --- a/pkg/manifest/local_server.go +++ b/pkg/manifest/local_server.go @@ -59,8 +59,8 @@ type LocalKVStore struct { type LocalKVStoreMap map[string]LocalKVStore // UnmarshalTOML performs custom unmarshalling of TOML data for LocalKVStoreMap. -func (m *LocalKVStoreMap) UnmarshalTOML(v interface{}) error { - raw, ok := v.(map[string]interface{}) +func (m *LocalKVStoreMap) UnmarshalTOML(v any) error { + raw, ok := v.(map[string]any) if !ok { return fmt.Errorf("expected kv_stores to be a TOML table") } @@ -69,10 +69,10 @@ func (m *LocalKVStoreMap) UnmarshalTOML(v interface{}) error { for key, val := range raw { switch typed := val.(type) { - case []interface{}: + case []any: var entries []KVStoreArrayEntry for _, item := range typed { - obj, ok := item.(map[string]interface{}) + obj, ok := item.(map[string]any) if !ok { return fmt.Errorf("invalid item in array for key %q", key) } @@ -87,7 +87,7 @@ func (m *LocalKVStoreMap) UnmarshalTOML(v interface{}) error { Array: entries, } - case map[string]interface{}: + case map[string]any: file, hasFile := typed["file"].(string) format, hasFormat := typed["format"].(string) @@ -139,8 +139,8 @@ type LocalSecretStore struct { type LocalSecretStoreMap map[string]LocalSecretStore // UnmarshalTOML performs custom unmarshalling of TOML data for LocalSecretStoreMap. -func (m *LocalSecretStoreMap) UnmarshalTOML(v interface{}) error { - raw, ok := v.(map[string]interface{}) +func (m *LocalSecretStoreMap) UnmarshalTOML(v any) error { + raw, ok := v.(map[string]any) if !ok { return fmt.Errorf("expected secret_stores to be a TOML table") } @@ -149,10 +149,10 @@ func (m *LocalSecretStoreMap) UnmarshalTOML(v interface{}) error { for key, val := range raw { switch typed := val.(type) { - case []interface{}: + case []any: var entries []SecretStoreArrayEntry for _, item := range typed { - obj, ok := item.(map[string]interface{}) + obj, ok := item.(map[string]any) if !ok { return fmt.Errorf("invalid item in array for key %q", key) } @@ -167,7 +167,7 @@ func (m *LocalSecretStoreMap) UnmarshalTOML(v interface{}) error { Array: entries, } - case map[string]interface{}: + case map[string]any: file, hasFile := typed["file"].(string) format, hasFormat := typed["format"].(string) @@ -191,7 +191,7 @@ func (m *LocalSecretStoreMap) UnmarshalTOML(v interface{}) error { return nil } -func decodeTOMLMap(m map[string]interface{}, out interface{}) error { +func decodeTOMLMap(m map[string]any, out any) error { buf := new(bytes.Buffer) enc := toml.NewEncoder(buf) if err := enc.Encode(m); err != nil { diff --git a/pkg/manifest/local_server_test.go b/pkg/manifest/local_server_test.go index badcd5303..882ba2b01 100644 --- a/pkg/manifest/local_server_test.go +++ b/pkg/manifest/local_server_test.go @@ -2,7 +2,7 @@ package manifest import ( "bytes" - "log" + "reflect" "strings" "testing" @@ -14,12 +14,12 @@ type KVWrapper struct { } func (w KVWrapper) MarshalTOML() ([]byte, error) { - obj := make(map[string]interface{}) - kv := make(map[string]interface{}) + obj := make(map[string]any) + kv := make(map[string]any) for key, entry := range w.KVStores { if entry.External != nil { - kv[key] = map[string]interface{}{ + kv[key] = map[string]any{ "file": entry.External.File, "format": entry.External.Format, } @@ -40,12 +40,12 @@ type SecretStoreWrapper struct { } func (w SecretStoreWrapper) MarshalTOML() ([]byte, error) { - obj := make(map[string]interface{}) - kv := make(map[string]interface{}) + obj := make(map[string]any) + kv := make(map[string]any) for key, entry := range w.SecretStores { if entry.External != nil { - kv[key] = map[string]interface{}{ + kv[key] = map[string]any{ "file": entry.External.File, "format": entry.External.Format, } @@ -63,23 +63,27 @@ func (w SecretStoreWrapper) MarshalTOML() ([]byte, error) { func TestLocalKVStores_UnmarshalTOML(t *testing.T) { tests := []struct { - name string - inputTOML string - expectError bool - expectArray bool - expectLength int - wantFile string + name string + inputTOML string + expectError bool + expected LocalKVStore }{ { name: "legacy array format", inputTOML: ` [[kv_stores.my-kv]] -key = "my-kv" +key = "kv" file = "kv.json" `, - expectArray: true, - expectLength: 1, - wantFile: "kv.json", + expected: LocalKVStore{ + IsArray: true, + Array: []KVStoreArrayEntry{ + { + Key: "kv", + File: "kv.json", + }, + }, + }, }, { name: "external file format", @@ -87,9 +91,13 @@ file = "kv.json" [kv_stores] my-kv = { file = "kv.json", format = "json" } `, - expectArray: false, - expectLength: 0, - wantFile: "kv.json", + expected: LocalKVStore{ + IsArray: false, + External: &KVStoreExternalFile{ + File: "kv.json", + Format: "json", + }, + }, }, { name: "invalid format", @@ -108,14 +116,6 @@ my-kv = "not-a-valid-entry" decoder := toml.NewDecoder(strings.NewReader(tt.inputTOML)) err := decoder.Decode(&m) - buf := new(bytes.Buffer) - encoder := toml.NewEncoder(buf) - - encodeErr := encoder.Encode(m) - if encodeErr != nil { - log.Fatalf("TOML encode failed: %v", encodeErr) - } - if tt.expectError { if err == nil { t.Fatal("Expected error for invalid format, but got none") @@ -130,24 +130,8 @@ my-kv = "not-a-valid-entry" t.Fatalf("Expected key 'my-kv' not found") } - if got.IsArray != tt.expectArray { - t.Fatalf("Expected IsArray=%v, got %v", tt.expectArray, got.IsArray) - } - - if tt.expectArray { - if len(got.Array) != tt.expectLength { - t.Fatalf("Expected %d inline entries, got %d", tt.expectLength, len(got.Array)) - } - if got.Array[0].File != tt.wantFile { - t.Errorf("Expected file %q, got %q", tt.wantFile, got.Array[0].File) - } - } else { - if got.External == nil { - t.Fatal("Expected KVStoreExternalFile but got nil") - } - if got.External.File != tt.wantFile { - t.Errorf("Expected file %q, got %q", tt.wantFile, got.External.File) - } + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("Mismatch!\nGot: %+v\nWant: %+v", got, tt.expected) } }) } @@ -155,12 +139,10 @@ my-kv = "not-a-valid-entry" func TestLocalSecretStores_UnmarshalTOML(t *testing.T) { tests := []struct { - name string - inputTOML string - expectError bool - expectArray bool - expectLength int - wantFile string + name string + inputTOML string + expectError bool + expected LocalSecretStore }{ { name: "legacy array format", @@ -169,19 +151,29 @@ func TestLocalSecretStores_UnmarshalTOML(t *testing.T) { key = "secret" file = "secret.json" `, - expectArray: true, - expectLength: 1, - wantFile: "secret.json", + expected: LocalSecretStore{ + IsArray: true, + Array: []SecretStoreArrayEntry{ + { + Key: "secret", + File: "secret.json", + }, + }, + }, }, { name: "external file format", inputTOML: ` [secret_stores] -my-secret-store = { file = "kv.json", format = "json" } +my-secret-store = { file = "secret.json", format = "json" } `, - expectArray: false, - expectLength: 0, - wantFile: "kv.json", + expected: LocalSecretStore{ + IsArray: false, + External: &SecretStoreExternalFile{ + File: "secret.json", + Format: "json", + }, + }, }, { name: "invalid format", @@ -200,14 +192,6 @@ my-secret-store = "not-a-valid-entry" decoder := toml.NewDecoder(strings.NewReader(tt.inputTOML)) err := decoder.Decode(&m) - buf := new(bytes.Buffer) - encoder := toml.NewEncoder(buf) - - encodeErr := encoder.Encode(m) - if encodeErr != nil { - log.Fatalf("TOML encode failed: %v", encodeErr) - } - if tt.expectError { if err == nil { t.Fatal("Expected error for invalid format, but got none") @@ -222,24 +206,8 @@ my-secret-store = "not-a-valid-entry" t.Fatalf("Expected key 'my-secret-store' not found") } - if got.IsArray != tt.expectArray { - t.Fatalf("Expected IsArray=%v, got %v", tt.expectArray, got.IsArray) - } - - if tt.expectArray { - if len(got.Array) != tt.expectLength { - t.Fatalf("Expected %d inline entries, got %d", tt.expectLength, len(got.Array)) - } - if got.Array[0].File != tt.wantFile { - t.Errorf("Expected file %q, got %q", tt.wantFile, got.Array[0].File) - } - } else { - if got.External == nil { - t.Fatal("Expected SecretStoreExternalFile but got nil") - } - if got.External.File != tt.wantFile { - t.Errorf("Expected file %q, got %q", tt.wantFile, got.External.File) - } + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("Mismatch!\nGot: %+v\nWant: %+v", got, tt.expected) } }) } From 54488bf4806621d06b6885a8145c711a30b0aee2 Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Thu, 3 Apr 2025 08:11:54 +0900 Subject: [PATCH 3/4] Replace wrapper structs in tests with local anonymous structs --- pkg/manifest/local_server_test.go | 61 +++---------------------------- 1 file changed, 6 insertions(+), 55 deletions(-) diff --git a/pkg/manifest/local_server_test.go b/pkg/manifest/local_server_test.go index 882ba2b01..573eb0dc8 100644 --- a/pkg/manifest/local_server_test.go +++ b/pkg/manifest/local_server_test.go @@ -1,7 +1,6 @@ package manifest import ( - "bytes" "reflect" "strings" "testing" @@ -9,58 +8,6 @@ import ( "github.com/pelletier/go-toml" ) -type KVWrapper struct { - KVStores LocalKVStoreMap `toml:"kv_stores"` -} - -func (w KVWrapper) MarshalTOML() ([]byte, error) { - obj := make(map[string]any) - kv := make(map[string]any) - - for key, entry := range w.KVStores { - if entry.External != nil { - kv[key] = map[string]any{ - "file": entry.External.File, - "format": entry.External.Format, - } - } else { - kv[key] = entry.Array - } - } - - obj["kv_stores"] = kv - - buf := new(bytes.Buffer) - err := toml.NewEncoder(buf).Encode(obj) - return buf.Bytes(), err -} - -type SecretStoreWrapper struct { - SecretStores LocalSecretStoreMap `toml:"secret_stores"` -} - -func (w SecretStoreWrapper) MarshalTOML() ([]byte, error) { - obj := make(map[string]any) - kv := make(map[string]any) - - for key, entry := range w.SecretStores { - if entry.External != nil { - kv[key] = map[string]any{ - "file": entry.External.File, - "format": entry.External.Format, - } - } else { - kv[key] = entry.Array - } - } - - obj["kv_stores"] = kv - - buf := new(bytes.Buffer) - err := toml.NewEncoder(buf).Encode(obj) - return buf.Bytes(), err -} - func TestLocalKVStores_UnmarshalTOML(t *testing.T) { tests := []struct { name string @@ -111,7 +58,9 @@ my-kv = "not-a-valid-entry" for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var m KVWrapper + var m struct { + KVStores LocalKVStoreMap `toml:"kv_stores"` + } decoder := toml.NewDecoder(strings.NewReader(tt.inputTOML)) err := decoder.Decode(&m) @@ -187,7 +136,9 @@ my-secret-store = "not-a-valid-entry" for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var m SecretStoreWrapper + var m struct { + SecretStores LocalSecretStoreMap `toml:"secret_stores"` + } decoder := toml.NewDecoder(strings.NewReader(tt.inputTOML)) err := decoder.Decode(&m) From 954950808c4ce3f01212494d5a101846c73d66f8 Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Thu, 3 Apr 2025 08:15:07 +0900 Subject: [PATCH 4/4] Add support for "Metadata" field --- CHANGELOG.md | 1 + pkg/manifest/file.go | 3 +++ pkg/manifest/local_server.go | 7 ++++--- pkg/manifest/local_server_test.go | 6 ++++-- pkg/manifest/testdata/fastly-viceroy-update.toml | 3 ++- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aefa65104..0b2c16237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Enhancements: - feat(config): Support file/format for kv_store and secret_store in fastly.toml +- feat(config): Support metadata for kv_store in fastly.toml ### Bug fixes: diff --git a/pkg/manifest/file.go b/pkg/manifest/file.go index 954de8696..c937eb36b 100644 --- a/pkg/manifest/file.go +++ b/pkg/manifest/file.go @@ -81,6 +81,9 @@ func (f *File) MarshalTOML() ([]byte, error) { if e.Data != "" { obj["data"] = e.Data } + if e.Metadata != "" { + obj["metadata"] = e.Metadata + } items = append(items, obj) } kvStores[key] = items diff --git a/pkg/manifest/local_server.go b/pkg/manifest/local_server.go index b66632369..5a1d87161 100644 --- a/pkg/manifest/local_server.go +++ b/pkg/manifest/local_server.go @@ -34,9 +34,10 @@ type LocalConfigStore struct { // KVStoreArrayEntry represents an array-based key/value store entries. // It expects a key plus either a data or file field. type KVStoreArrayEntry struct { - Key string `toml:"key"` - File string `toml:"file,omitempty"` - Data string `toml:"data,omitempty"` + Key string `toml:"key"` + File string `toml:"file,omitempty"` + Data string `toml:"data,omitempty"` + Metadata string `toml:"metadata,omitempty"` } // KVStoreExternalFile represents the external key/value store, diff --git a/pkg/manifest/local_server_test.go b/pkg/manifest/local_server_test.go index 573eb0dc8..2e6371b0e 100644 --- a/pkg/manifest/local_server_test.go +++ b/pkg/manifest/local_server_test.go @@ -21,13 +21,15 @@ func TestLocalKVStores_UnmarshalTOML(t *testing.T) { [[kv_stores.my-kv]] key = "kv" file = "kv.json" +metadata = "metadata" `, expected: LocalKVStore{ IsArray: true, Array: []KVStoreArrayEntry{ { - Key: "kv", - File: "kv.json", + Key: "kv", + File: "kv.json", + Metadata: "metadata", }, }, }, diff --git a/pkg/manifest/testdata/fastly-viceroy-update.toml b/pkg/manifest/testdata/fastly-viceroy-update.toml index 9434a538b..c7e2e86ce 100644 --- a/pkg/manifest/testdata/fastly-viceroy-update.toml +++ b/pkg/manifest/testdata/fastly-viceroy-update.toml @@ -37,7 +37,7 @@ qux""" [local_server.kv_stores] store_one = [ - { key = "first", data = "This is some data" }, + { key = "first", data = "This is some data", metadata = "This is some metadata" }, { key = "second", file = "strings.json" }, ] store_three = { file = "path/to/kv.json", format = "json" } @@ -45,6 +45,7 @@ store_three = { file = "path/to/kv.json", format = "json" } [[local_server.kv_stores.store_two]] key = "first" data = "This is some data" +metadata = "This is some metadata" [[local_server.kv_stores.store_two]] key = "second"