Skip to content

Commit cf4af94

Browse files
authored
Support new file/format model shape for local_server.kv_store and secret_store (as well as metadata in kv_store) (#1446)
* Support new file/format model shape for local_server.kv_store and secret_store * Support new metadata field in model shape for local_server.kv_store
1 parent fe78c10 commit cf4af94

5 files changed

Lines changed: 449 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
### Enhancements:
88

9+
- feat(config): Support file/format for kv_store and secret_store in fastly.toml
10+
- feat(config): Support metadata for kv_store in fastly.toml
11+
912
### Bug fixes:
1013

1114
### Dependencies:

pkg/manifest/file.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package manifest
22

33
import (
44
"bufio"
5+
"bytes"
56
"fmt"
67
"io"
78
"os"
@@ -50,6 +51,108 @@ type File struct {
5051
readError error
5152
}
5253

54+
// MarshalTOML performs custom marshalling to TOML for objects of File type.
55+
func (f *File) MarshalTOML() ([]byte, error) {
56+
localServer := make(map[string]any)
57+
58+
if f.LocalServer.Backends != nil {
59+
localServer["backends"] = f.LocalServer.Backends
60+
}
61+
62+
if f.LocalServer.ConfigStores != nil {
63+
localServer["config_stores"] = f.LocalServer.ConfigStores
64+
}
65+
66+
if f.LocalServer.KVStores != nil {
67+
kvStores := make(map[string]any)
68+
for key, entry := range f.LocalServer.KVStores {
69+
if entry.External != nil {
70+
kvStores[key] = map[string]any{
71+
"file": entry.External.File,
72+
"format": entry.External.Format,
73+
}
74+
} else {
75+
items := make([]map[string]any, 0, len(entry.Array))
76+
for _, e := range entry.Array {
77+
obj := map[string]any{"key": e.Key}
78+
if e.File != "" {
79+
obj["file"] = e.File
80+
}
81+
if e.Data != "" {
82+
obj["data"] = e.Data
83+
}
84+
if e.Metadata != "" {
85+
obj["metadata"] = e.Metadata
86+
}
87+
items = append(items, obj)
88+
}
89+
kvStores[key] = items
90+
}
91+
}
92+
localServer["kv_stores"] = kvStores
93+
}
94+
95+
if f.LocalServer.SecretStores != nil {
96+
secretStores := make(map[string]any)
97+
for key, entry := range f.LocalServer.SecretStores {
98+
if entry.External != nil {
99+
secretStores[key] = map[string]any{
100+
"file": entry.External.File,
101+
"format": entry.External.Format,
102+
}
103+
} else {
104+
items := make([]map[string]any, 0, len(entry.Array))
105+
for _, e := range entry.Array {
106+
obj := map[string]any{"key": e.Key}
107+
if e.File != "" {
108+
obj["file"] = e.File
109+
}
110+
if e.Data != "" {
111+
obj["data"] = e.Data
112+
}
113+
items = append(items, obj)
114+
}
115+
secretStores[key] = items
116+
}
117+
}
118+
localServer["secret_stores"] = secretStores
119+
}
120+
121+
if f.LocalServer.ViceroyVersion != "" {
122+
localServer["viceroy_version"] = f.LocalServer.ViceroyVersion
123+
}
124+
125+
out := struct {
126+
Authors []string `toml:"authors"`
127+
ClonedFrom string `toml:"cloned_from,omitempty"`
128+
Description string `toml:"description"`
129+
Language string `toml:"language"`
130+
Profile string `toml:"profile,omitempty"`
131+
LocalServer any `toml:"local_server"` // override this field
132+
ManifestVersion Version `toml:"manifest_version"`
133+
Name string `toml:"name"`
134+
Scripts Scripts `toml:"scripts,omitempty"`
135+
ServiceID string `toml:"service_id"`
136+
Setup Setup `toml:"setup,omitempty"`
137+
}{
138+
Authors: f.Authors,
139+
ClonedFrom: f.ClonedFrom,
140+
Description: f.Description,
141+
Language: f.Language,
142+
Profile: f.Profile,
143+
LocalServer: localServer,
144+
ManifestVersion: f.ManifestVersion,
145+
Name: f.Name,
146+
Scripts: f.Scripts,
147+
ServiceID: f.ServiceID,
148+
Setup: f.Setup,
149+
}
150+
151+
var buf bytes.Buffer
152+
err := toml.NewEncoder(&buf).Encode(out)
153+
return buf.Bytes(), err
154+
}
155+
53156
// Exists yields whether the manifest exists.
54157
//
55158
// Specifically, it indicates that a toml.Unmarshal() of the toml disk content

pkg/manifest/local_server.go

Lines changed: 172 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package manifest
22

3+
import (
4+
"bytes"
5+
"fmt"
6+
7+
"github.com/pelletier/go-toml"
8+
)
9+
310
// LocalServer represents a list of mocked Viceroy resources.
411
type LocalServer struct {
5-
Backends map[string]LocalBackend `toml:"backends"`
6-
ConfigStores map[string]LocalConfigStore `toml:"config_stores,omitempty"`
7-
KVStores map[string][]LocalKVStore `toml:"kv_stores,omitempty"`
8-
SecretStores map[string][]LocalSecretStore `toml:"secret_stores,omitempty"`
9-
ViceroyVersion string `toml:"viceroy_version,omitempty"`
12+
Backends map[string]LocalBackend `toml:"backends"`
13+
ConfigStores map[string]LocalConfigStore `toml:"config_stores,omitempty"`
14+
KVStores LocalKVStoreMap `toml:"kv_stores,omitempty"`
15+
SecretStores LocalSecretStoreMap `toml:"secret_stores,omitempty"`
16+
ViceroyVersion string `toml:"viceroy_version,omitempty"`
1017
}
1118

1219
// LocalBackend represents a backend to be mocked by the local testing server.
@@ -24,16 +31,172 @@ type LocalConfigStore struct {
2431
Contents map[string]string `toml:"contents,omitempty"`
2532
}
2633

27-
// LocalKVStore represents an kv_store to be mocked by the local testing server.
34+
// KVStoreArrayEntry represents an array-based key/value store entries.
35+
// It expects a key plus either a data or file field.
36+
type KVStoreArrayEntry struct {
37+
Key string `toml:"key"`
38+
File string `toml:"file,omitempty"`
39+
Data string `toml:"data,omitempty"`
40+
Metadata string `toml:"metadata,omitempty"`
41+
}
42+
43+
// KVStoreExternalFile represents the external key/value store,
44+
// which must have both a file and a format.
45+
type KVStoreExternalFile struct {
46+
File string `toml:"file"`
47+
Format string `toml:"format"`
48+
}
49+
50+
// LocalKVStore represents a kv_store to be mocked by the local testing server.
51+
// It is a union type and can either be an array of KVStoreArrayEntry or a single KVStoreExternalFile.
52+
// The IsArray flag is used to preserve the original input style.
2853
type LocalKVStore struct {
54+
IsArray bool `toml:"-"`
55+
Array []KVStoreArrayEntry `toml:"-"`
56+
External *KVStoreExternalFile `toml:"-"`
57+
}
58+
59+
// LocalKVStoreMap is a map of kv_store names to the local kv_store representation.
60+
type LocalKVStoreMap map[string]LocalKVStore
61+
62+
// UnmarshalTOML performs custom unmarshalling of TOML data for LocalKVStoreMap.
63+
func (m *LocalKVStoreMap) UnmarshalTOML(v any) error {
64+
raw, ok := v.(map[string]any)
65+
if !ok {
66+
return fmt.Errorf("expected kv_stores to be a TOML table")
67+
}
68+
69+
result := make(LocalKVStoreMap)
70+
71+
for key, val := range raw {
72+
switch typed := val.(type) {
73+
case []any:
74+
var entries []KVStoreArrayEntry
75+
for _, item := range typed {
76+
obj, ok := item.(map[string]any)
77+
if !ok {
78+
return fmt.Errorf("invalid item in array for key %q", key)
79+
}
80+
var arrayEntry KVStoreArrayEntry
81+
if err := decodeTOMLMap(obj, &arrayEntry); err != nil {
82+
return fmt.Errorf("decode failed for array item in key %q: %w", key, err)
83+
}
84+
entries = append(entries, arrayEntry)
85+
}
86+
result[key] = LocalKVStore{
87+
IsArray: true,
88+
Array: entries,
89+
}
90+
91+
case map[string]any:
92+
file, hasFile := typed["file"].(string)
93+
format, hasFormat := typed["format"].(string)
94+
95+
if !hasFile || !hasFormat {
96+
return fmt.Errorf("key %q must have both file and format", key)
97+
}
98+
result[key] = LocalKVStore{
99+
IsArray: false,
100+
External: &KVStoreExternalFile{
101+
File: file,
102+
Format: format,
103+
},
104+
}
105+
106+
default:
107+
return fmt.Errorf("unsupported value type for key %q: %T", key, typed)
108+
}
109+
}
110+
111+
*m = result
112+
return nil
113+
}
114+
115+
// SecretStoreArrayEntry represents an array-based key/value store entries.
116+
// It expects a key plus either a data or file field.
117+
type SecretStoreArrayEntry struct {
29118
Key string `toml:"key"`
30119
File string `toml:"file,omitempty"`
31120
Data string `toml:"data,omitempty"`
32121
}
33122

123+
// SecretStoreExternalFile represents the external key/value store,
124+
// which must have both a file and a format.
125+
type SecretStoreExternalFile struct {
126+
File string `toml:"file"`
127+
Format string `toml:"format"`
128+
}
129+
34130
// LocalSecretStore represents a secret_store to be mocked by the local testing server.
131+
// It is a union type and can either be an array of SecretStoreArrayEntry or a single SecretStoreExternalFile.
132+
// The IsArray flag is used to preserve the original input style.
35133
type LocalSecretStore struct {
36-
Key string `toml:"key"`
37-
File string `toml:"file,omitempty"`
38-
Data string `toml:"data,omitempty"`
134+
IsArray bool `toml:"-"`
135+
Array []SecretStoreArrayEntry `toml:"-"`
136+
External *SecretStoreExternalFile `toml:"-"`
137+
}
138+
139+
// LocalSecretStoreMap is a map of secret_store names to the local secret_store representation.
140+
type LocalSecretStoreMap map[string]LocalSecretStore
141+
142+
// UnmarshalTOML performs custom unmarshalling of TOML data for LocalSecretStoreMap.
143+
func (m *LocalSecretStoreMap) UnmarshalTOML(v any) error {
144+
raw, ok := v.(map[string]any)
145+
if !ok {
146+
return fmt.Errorf("expected secret_stores to be a TOML table")
147+
}
148+
149+
result := make(LocalSecretStoreMap)
150+
151+
for key, val := range raw {
152+
switch typed := val.(type) {
153+
case []any:
154+
var entries []SecretStoreArrayEntry
155+
for _, item := range typed {
156+
obj, ok := item.(map[string]any)
157+
if !ok {
158+
return fmt.Errorf("invalid item in array for key %q", key)
159+
}
160+
var arrayEntry SecretStoreArrayEntry
161+
if err := decodeTOMLMap(obj, &arrayEntry); err != nil {
162+
return fmt.Errorf("decode failed for array item in key %q: %w", key, err)
163+
}
164+
entries = append(entries, arrayEntry)
165+
}
166+
result[key] = LocalSecretStore{
167+
IsArray: true,
168+
Array: entries,
169+
}
170+
171+
case map[string]any:
172+
file, hasFile := typed["file"].(string)
173+
format, hasFormat := typed["format"].(string)
174+
175+
if !hasFile || !hasFormat {
176+
return fmt.Errorf("key %q must have both file and format", key)
177+
}
178+
result[key] = LocalSecretStore{
179+
IsArray: false,
180+
External: &SecretStoreExternalFile{
181+
File: file,
182+
Format: format,
183+
},
184+
}
185+
186+
default:
187+
return fmt.Errorf("unsupported value type for key %q: %T", key, typed)
188+
}
189+
}
190+
191+
*m = result
192+
return nil
193+
}
194+
195+
func decodeTOMLMap(m map[string]any, out any) error {
196+
buf := new(bytes.Buffer)
197+
enc := toml.NewEncoder(buf)
198+
if err := enc.Encode(m); err != nil {
199+
return err
200+
}
201+
return toml.NewDecoder(buf).Decode(out)
39202
}

0 commit comments

Comments
 (0)