Skip to content

Commit 7f58837

Browse files
committed
Support file/format for kv_store and secret_store
1 parent 692eff0 commit 7f58837

5 files changed

Lines changed: 523 additions & 9 deletions

File tree

CHANGELOG.md

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

77
### Enhancements:
88

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

1113
### Dependencies:

pkg/manifest/file.go

Lines changed: 102 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,107 @@ 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]interface{})
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]interface{})
68+
for key, entry := range f.LocalServer.KVStores {
69+
if entry.External != nil {
70+
kvStores[key] = map[string]interface{}{
71+
"file": entry.External.File,
72+
"format": entry.External.Format,
73+
}
74+
} else {
75+
items := make([]map[string]interface{}, 0, len(entry.Array))
76+
for _, e := range entry.Array {
77+
obj := map[string]interface{}{"key": e.Key}
78+
if e.File != "" {
79+
obj["file"] = e.File
80+
}
81+
if e.Data != "" {
82+
obj["data"] = e.Data
83+
}
84+
items = append(items, obj)
85+
}
86+
kvStores[key] = items
87+
}
88+
}
89+
localServer["kv_stores"] = kvStores
90+
}
91+
92+
if f.LocalServer.SecretStores != nil {
93+
secretStores := make(map[string]interface{})
94+
for key, entry := range f.LocalServer.SecretStores {
95+
if entry.External != nil {
96+
secretStores[key] = map[string]interface{}{
97+
"file": entry.External.File,
98+
"format": entry.External.Format,
99+
}
100+
} else {
101+
items := make([]map[string]interface{}, 0, len(entry.Array))
102+
for _, e := range entry.Array {
103+
obj := map[string]interface{}{"key": e.Key}
104+
if e.File != "" {
105+
obj["file"] = e.File
106+
}
107+
if e.Data != "" {
108+
obj["data"] = e.Data
109+
}
110+
items = append(items, obj)
111+
}
112+
secretStores[key] = items
113+
}
114+
}
115+
localServer["secret_stores"] = secretStores
116+
}
117+
118+
if f.LocalServer.ViceroyVersion != "" {
119+
localServer["viceroy_version"] = f.LocalServer.ViceroyVersion
120+
}
121+
122+
type outputFile struct {
123+
Authors []string `toml:"authors"`
124+
ClonedFrom string `toml:"cloned_from,omitempty"`
125+
Description string `toml:"description"`
126+
Language string `toml:"language"`
127+
Profile string `toml:"profile,omitempty"`
128+
LocalServer interface{} `toml:"local_server"` // override this field
129+
ManifestVersion Version `toml:"manifest_version"`
130+
Name string `toml:"name"`
131+
Scripts Scripts `toml:"scripts,omitempty"`
132+
ServiceID string `toml:"service_id"`
133+
Setup Setup `toml:"setup,omitempty"`
134+
}
135+
136+
out := outputFile{
137+
Authors: f.Authors,
138+
ClonedFrom: f.ClonedFrom,
139+
Description: f.Description,
140+
Language: f.Language,
141+
Profile: f.Profile,
142+
LocalServer: localServer,
143+
ManifestVersion: f.ManifestVersion,
144+
Name: f.Name,
145+
Scripts: f.Scripts,
146+
ServiceID: f.ServiceID,
147+
Setup: f.Setup,
148+
}
149+
150+
var buf bytes.Buffer
151+
err := toml.NewEncoder(&buf).Encode(out)
152+
return buf.Bytes(), err
153+
}
154+
53155
// Exists yields whether the manifest exists.
54156
//
55157
// Specifically, it indicates that a toml.Unmarshal() of the toml disk content

pkg/manifest/local_server.go

Lines changed: 171 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,171 @@ 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.
28-
type LocalKVStore struct {
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 {
2937
Key string `toml:"key"`
3038
File string `toml:"file,omitempty"`
3139
Data string `toml:"data,omitempty"`
3240
}
3341

34-
// LocalSecretStore represents a secret_store to be mocked by the local testing server.
35-
type LocalSecretStore struct {
42+
// KVStoreExternalFile represents the external key/value store,
43+
// which must have both a file and a format.
44+
type KVStoreExternalFile struct {
45+
File string `toml:"file"`
46+
Format string `toml:"format"`
47+
}
48+
49+
// LocalKVStore represents a kv_store to be mocked by the local testing server.
50+
// It is a union type and can either be an array of KVStoreArrayEntry or a single KVStoreExternalFile.
51+
// The IsArray flag is used to preserve the original input style.
52+
type LocalKVStore struct {
53+
IsArray bool `toml:"-"`
54+
Array []KVStoreArrayEntry `toml:"-"`
55+
External *KVStoreExternalFile `toml:"-"`
56+
}
57+
58+
// LocalKVStoreMap is a map of kv_store names to the local kv_store representation.
59+
type LocalKVStoreMap map[string]LocalKVStore
60+
61+
// UnmarshalTOML performs custom unmarshalling of TOML data for LocalKVStoreMap.
62+
func (m *LocalKVStoreMap) UnmarshalTOML(v interface{}) error {
63+
raw, ok := v.(map[string]interface{})
64+
if !ok {
65+
return fmt.Errorf("expected kv_stores to be a TOML table")
66+
}
67+
68+
result := make(LocalKVStoreMap)
69+
70+
for key, val := range raw {
71+
switch typed := val.(type) {
72+
case []interface{}:
73+
var entries []KVStoreArrayEntry
74+
for _, item := range typed {
75+
obj, ok := item.(map[string]interface{})
76+
if !ok {
77+
return fmt.Errorf("invalid item in array for key %q", key)
78+
}
79+
var arrayEntry KVStoreArrayEntry
80+
if err := decodeTOMLMap(obj, &arrayEntry); err != nil {
81+
return fmt.Errorf("decode failed for array item in key %q: %w", key, err)
82+
}
83+
entries = append(entries, arrayEntry)
84+
}
85+
result[key] = LocalKVStore{
86+
IsArray: true,
87+
Array: entries,
88+
}
89+
90+
case map[string]interface{}:
91+
file, hasFile := typed["file"].(string)
92+
format, hasFormat := typed["format"].(string)
93+
94+
if !hasFile || !hasFormat {
95+
return fmt.Errorf("key %q must have both file and format", key)
96+
}
97+
result[key] = LocalKVStore{
98+
IsArray: false,
99+
External: &KVStoreExternalFile{
100+
File: file,
101+
Format: format,
102+
},
103+
}
104+
105+
default:
106+
return fmt.Errorf("unsupported value type for key %q: %T", key, typed)
107+
}
108+
}
109+
110+
*m = result
111+
return nil
112+
}
113+
114+
// SecretStoreArrayEntry represents an array-based key/value store entries.
115+
// It expects a key plus either a data or file field.
116+
type SecretStoreArrayEntry struct {
36117
Key string `toml:"key"`
37118
File string `toml:"file,omitempty"`
38119
Data string `toml:"data,omitempty"`
39120
}
121+
122+
// SecretStoreExternalFile represents the external key/value store,
123+
// which must have both a file and a format.
124+
type SecretStoreExternalFile struct {
125+
File string `toml:"file"`
126+
Format string `toml:"format"`
127+
}
128+
129+
// LocalSecretStore represents a secret_store to be mocked by the local testing server.
130+
// It is a union type and can either be an array of SecretStoreArrayEntry or a single SecretStoreExternalFile.
131+
// The IsArray flag is used to preserve the original input style.
132+
type LocalSecretStore struct {
133+
IsArray bool `toml:"-"`
134+
Array []SecretStoreArrayEntry `toml:"-"`
135+
External *SecretStoreExternalFile `toml:"-"`
136+
}
137+
138+
// LocalSecretStoreMap is a map of secret_store names to the local secret_store representation.
139+
type LocalSecretStoreMap map[string]LocalSecretStore
140+
141+
// UnmarshalTOML performs custom unmarshalling of TOML data for LocalSecretStoreMap.
142+
func (m *LocalSecretStoreMap) UnmarshalTOML(v interface{}) error {
143+
raw, ok := v.(map[string]interface{})
144+
if !ok {
145+
return fmt.Errorf("expected secret_stores to be a TOML table")
146+
}
147+
148+
result := make(LocalSecretStoreMap)
149+
150+
for key, val := range raw {
151+
switch typed := val.(type) {
152+
case []interface{}:
153+
var entries []SecretStoreArrayEntry
154+
for _, item := range typed {
155+
obj, ok := item.(map[string]interface{})
156+
if !ok {
157+
return fmt.Errorf("invalid item in array for key %q", key)
158+
}
159+
var arrayEntry SecretStoreArrayEntry
160+
if err := decodeTOMLMap(obj, &arrayEntry); err != nil {
161+
return fmt.Errorf("decode failed for array item in key %q: %w", key, err)
162+
}
163+
entries = append(entries, arrayEntry)
164+
}
165+
result[key] = LocalSecretStore{
166+
IsArray: true,
167+
Array: entries,
168+
}
169+
170+
case map[string]interface{}:
171+
file, hasFile := typed["file"].(string)
172+
format, hasFormat := typed["format"].(string)
173+
174+
if !hasFile || !hasFormat {
175+
return fmt.Errorf("key %q must have both file and format", key)
176+
}
177+
result[key] = LocalSecretStore{
178+
IsArray: false,
179+
External: &SecretStoreExternalFile{
180+
File: file,
181+
Format: format,
182+
},
183+
}
184+
185+
default:
186+
return fmt.Errorf("unsupported value type for key %q: %T", key, typed)
187+
}
188+
}
189+
190+
*m = result
191+
return nil
192+
}
193+
194+
func decodeTOMLMap(m map[string]interface{}, out interface{}) error {
195+
buf := new(bytes.Buffer)
196+
enc := toml.NewEncoder(buf)
197+
if err := enc.Encode(m); err != nil {
198+
return err
199+
}
200+
return toml.NewDecoder(buf).Decode(out)
201+
}

0 commit comments

Comments
 (0)