Skip to content

Commit a61e5a3

Browse files
authored
Merge pull request #69 from OpenCHAMI/synackd/yaml-marshallers
feat(cistore): add `MarshalYAML()` and `UnmarshalYAML()` to `CloudConfigFile`
2 parents d532ac7 + fe1d1a1 commit a61e5a3

File tree

3 files changed

+172
-9
lines changed

3 files changed

+172
-9
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/rs/zerolog v1.33.0
1313
github.com/stretchr/testify v1.10.0
1414
github.com/swaggo/swag v1.16.4
15+
gopkg.in/yaml.v3 v3.0.1
1516
)
1617

1718
require (
@@ -40,7 +41,6 @@ require (
4041
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
4142
github.com/segmentio/asm v1.2.0 // indirect
4243
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
43-
gopkg.in/yaml.v3 v3.0.1 // indirect
4444
)
4545

4646
require (

pkg/cistore/models.go

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package cistore
33
import (
44
"encoding/json"
55
"errors"
6+
"fmt"
7+
8+
"gopkg.in/yaml.v3"
69

710
base "github.com/Cray-HPE/hms-base"
811
)
@@ -65,12 +68,12 @@ type ClusterDefaults struct {
6568
}
6669

6770
type CloudConfigFile struct {
68-
Content []byte `json:"content" swaggertype:"string" example:"IyMgdGVtcGxhdGU6IGppbmphCiNjbG91ZC1jb25maWcKbWVyZ2VfaG93OgotIG5hbWU6IGxpc3QKICBzZXR0aW5nczogW2FwcGVuZF0KLSBuYW1lOiBkaWN0CiAgc2V0dGluZ3M6IFtub19yZXBsYWNlLCByZWN1cnNlX2xpc3RdCnVzZXJzOgogIC0gbmFtZTogcm9vdAogICAgc3NoX2F1dGhvcml6ZWRfa2V5czoge3sgZHMubWV0YV9kYXRhLmluc3RhbmNlX2RhdGEudjEucHVibGljX2tleXMgfX0KZGlzYWJsZV9yb290OiBmYWxzZQo=" description:"Cloud-Init configuration content whose encoding depends on the value of 'encoding'"`
69-
Name string `json:"filename"`
70-
Encoding string `json:"encoding,omitempty" enums:"base64,plain"`
71+
Content []byte `json:"content" yaml:"content" swaggertype:"string" example:"IyMgdGVtcGxhdGU6IGppbmphCiNjbG91ZC1jb25maWcKbWVyZ2VfaG93OgotIG5hbWU6IGxpc3QKICBzZXR0aW5nczogW2FwcGVuZF0KLSBuYW1lOiBkaWN0CiAgc2V0dGluZ3M6IFtub19yZXBsYWNlLCByZWN1cnNlX2xpc3RdCnVzZXJzOgogIC0gbmFtZTogcm9vdAogICAgc3NoX2F1dGhvcml6ZWRfa2V5czoge3sgZHMubWV0YV9kYXRhLmluc3RhbmNlX2RhdGEudjEucHVibGljX2tleXMgfX0KZGlzYWJsZV9yb290OiBmYWxzZQo=" description:"Cloud-Init configuration content whose encoding depends on the value of 'encoding'"`
72+
Name string `json:"filename" yaml:"filename"`
73+
Encoding string `json:"encoding,omitempty" yaml:"encoding,omitempty" enums:"base64,plain"`
7174
}
7275

73-
// Custom unmarshaler for CloudConfigFile
76+
// Custom JSON unmarshaler for CloudConfigFile
7477
func (f *CloudConfigFile) UnmarshalJSON(data []byte) error {
7578
// Use an auxiliary struct so that:
7679
//
@@ -96,11 +99,57 @@ func (f *CloudConfigFile) UnmarshalJSON(data []byte) error {
9699
return nil
97100
}
98101

99-
// Custom marshaler for CloudConfigFile
102+
// Custom YAML unmarshaller for CloudConfigFile. This is needed because the yaml
103+
// library cannot unmarshal string to []byte like json can, so it needs to be
104+
// told how to do so.
105+
func (f *CloudConfigFile) UnmarshalYAML(n *yaml.Node) error {
106+
// Use an auxiliary struct so that:
107+
//
108+
// 1. json.Unmarshal doesn't recurse forever and overflow the stack.
109+
// 2. json.Unmarshal doesn't try to base64-decode "content" in the data
110+
// before assigning the bytes to f.Content. Content is unmarshalled
111+
// as a string instead of bytes in order to prevent this. After
112+
// unmarshalling, the string is converted back to bytes and assigned
113+
// to f.Content.
114+
//
115+
// Interestingly, yaml.Unmarshal will not unmarshal into aux's pointer
116+
// to f, so we have to use json.Unmarshal.
117+
type Alias CloudConfigFile
118+
aux := &struct {
119+
Content string `json:"content"`
120+
*Alias
121+
}{
122+
Alias: (*Alias)(f),
123+
}
124+
125+
// Decode YAML document (n) into aux struct, using a map as an
126+
// intermediary (since n.Decode() cannot decode into a []byte). We have
127+
// to use JSON for the unmarshalling (and, by consequence, the
128+
// marshalling) so that f will get written via aux's pointer to it. For
129+
// some reason, the yaml unmarshaller will not do that.
130+
//
131+
// 1. Decode YAML document (n) into map.
132+
// 2. JSON marshal map into bytes.
133+
// 3. JSON unmarshal bytes into aux struct.
134+
// 4. Set f.Content to byte-ified aux.Content.
135+
var mAux map[string]interface{}
136+
if err := n.Decode(&mAux); err != nil {
137+
return err
138+
}
139+
t, err := json.Marshal(mAux)
140+
if err != nil {
141+
return err
142+
}
143+
if err := json.Unmarshal(t, &aux); err != nil {
144+
return err
145+
}
146+
f.Content = []byte(aux.Content)
147+
148+
return nil
149+
}
150+
151+
// Custom JSON marshaler for CloudConfigFile
100152
func (f CloudConfigFile) MarshalJSON() ([]byte, error) {
101-
// Use temporary struct to marshal so json.Marshal doesn't recurse
102-
// indefinitely. Also to convert Content from bytes to string so
103-
// json.Marshal doesn't try to base64 encode the bytes.
104153
// Use an auxiliary struct so that:
105154
//
106155
// 1. json.Marshal doesn't recurse forever and overflow the stack.
@@ -118,3 +167,51 @@ func (f CloudConfigFile) MarshalJSON() ([]byte, error) {
118167
aux.Content = string(f.Content)
119168
return json.Marshal(aux)
120169
}
170+
171+
// Custom YAML marshaler for CloudConfigFile. This is needed because the yaml
172+
// library will not marshal []byte into string like json will, so it needs to be
173+
// told how to do so.
174+
func (f CloudConfigFile) MarshalYAML() (interface{}, error) {
175+
// Use an auxiliary struct so that:
176+
//
177+
// 1. yaml.Marshal doesn't recurse forever and overflow the stack.
178+
// 2. yaml.Marshal doesn't try to base64-encode f.Content. f.Content is
179+
// converted from bytes to a string and then assigned to aux.Content
180+
// to prevent this. Then, aux gets marshalled instead of f.
181+
//
182+
// aux is set to the values of f, but has its own string Content, set to
183+
// the stringified f.Content.
184+
type Alias CloudConfigFile
185+
aux := &struct {
186+
Content string `yaml:"content"`
187+
Alias
188+
}{
189+
Content: string(f.Content),
190+
Alias: (Alias)(f),
191+
}
192+
193+
// Convert aux into map, which has an "alias" key containing the
194+
// "content" that will be set to the actual string value. The map that
195+
// is mapped to "alias" is what is returned.
196+
//
197+
// 1. YAML marshal aux to bytes.
198+
// 2. YAML unmarshal bytes into map.
199+
// 3. Set content in "alias" to aux.Content.
200+
// 4. Return "alias" map.
201+
t, err := yaml.Marshal(aux)
202+
if err != nil {
203+
return nil, err
204+
}
205+
var nMap map[string]interface{}
206+
if err := yaml.Unmarshal(t, &nMap); err != nil {
207+
return nil, err
208+
}
209+
switch c := (nMap["alias"]).(type) {
210+
case map[string]interface{}:
211+
c["content"] = aux.Content
212+
default:
213+
return nil, fmt.Errorf("cloud config file in map is unknown type: wanted=(map[string]interface{}) actual=(%v)", c)
214+
}
215+
216+
return nMap["alias"], nil
217+
}

pkg/cistore/models_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cistore
33
import (
44
"encoding/base64"
55
"encoding/json"
6+
"gopkg.in/yaml.v3"
67
"testing"
78

89
"github.com/stretchr/testify/assert"
@@ -23,6 +24,19 @@ func TestCloudConfigFile_UnmarshalJSON_Plain(t *testing.T) {
2324
assert.Equal(t, []byte("#cloud-config\nusers:\n - name: test"), f.Content)
2425
}
2526

27+
func TestCloudConfigFile_UnmarshalYAML_Plain(t *testing.T) {
28+
yamlData := []byte(`filename: myconfig.yaml
29+
encoding: plain
30+
content: "#cloud-config\nusers:\n - name: test"`)
31+
32+
var f CloudConfigFile
33+
err := yaml.Unmarshal(yamlData, &f)
34+
assert.NoError(t, err)
35+
assert.Equal(t, "myconfig.yaml", f.Name)
36+
assert.Equal(t, "plain", f.Encoding)
37+
assert.Equal(t, []byte("#cloud-config\nusers:\n - name: test"), f.Content)
38+
}
39+
2640
func TestCloudConfigFile_UnmarshalJSON_Base64(t *testing.T) {
2741
encodedContent := base64.StdEncoding.EncodeToString([]byte("#cloud-config\nusers:\n - name: test"))
2842
jsonData := []byte(`{
@@ -40,6 +54,20 @@ func TestCloudConfigFile_UnmarshalJSON_Base64(t *testing.T) {
4054
assert.Equal(t, []byte(encodedContent), f.Content)
4155
}
4256

57+
func TestCloudConfigFile_UnmarshalYAML_Base64(t *testing.T) {
58+
encodedContent := base64.StdEncoding.EncodeToString([]byte("#cloud-config\nusers:\n - name: test"))
59+
yamlData := []byte(`filename: myconfig.yaml
60+
encoding: base64
61+
content: ` + encodedContent)
62+
63+
var f CloudConfigFile
64+
err := yaml.Unmarshal(yamlData, &f)
65+
assert.NoError(t, err)
66+
assert.Equal(t, "myconfig.yaml", f.Name)
67+
assert.Equal(t, "base64", f.Encoding)
68+
assert.Equal(t, []byte(encodedContent), f.Content)
69+
}
70+
4371
func TestCloudConfigFile_MarshalJSON_Plain(t *testing.T) {
4472
f := CloudConfigFile{
4573
Name: "plainconfig.yaml",
@@ -58,6 +86,24 @@ func TestCloudConfigFile_MarshalJSON_Plain(t *testing.T) {
5886
assert.Equal(t, "#cloud-config\nusers:\n - name: test", out["content"])
5987
}
6088

89+
func TestCloudConfigFile_MarshalYAML_Plain(t *testing.T) {
90+
f := CloudConfigFile{
91+
Name: "plainconfig.yaml",
92+
Encoding: "plain",
93+
Content: []byte("#cloud-config\nusers:\n - name: test"),
94+
}
95+
96+
data, err := yaml.Marshal(f)
97+
assert.NoError(t, err)
98+
99+
var out map[string]interface{}
100+
err = yaml.Unmarshal(data, &out)
101+
assert.NoError(t, err)
102+
assert.Equal(t, "plainconfig.yaml", out["filename"])
103+
assert.Equal(t, "plain", out["encoding"])
104+
assert.Equal(t, "#cloud-config\nusers:\n - name: test", out["content"])
105+
}
106+
61107
func TestCloudConfigFile_MarshalJSON_Base64(t *testing.T) {
62108
originalConfig := "#cloud-config\nusers:\n - name: test"
63109
b64Config := base64.StdEncoding.EncodeToString([]byte(originalConfig))
@@ -77,3 +123,23 @@ func TestCloudConfigFile_MarshalJSON_Base64(t *testing.T) {
77123
assert.Equal(t, "base64", out["encoding"])
78124
assert.Equal(t, b64Config, out["content"])
79125
}
126+
127+
func TestCloudConfigFile_MarshalYAML_Base64(t *testing.T) {
128+
originalConfig := "#cloud-config\nusers:\n - name: test"
129+
b64Config := base64.StdEncoding.EncodeToString([]byte(originalConfig))
130+
f := CloudConfigFile{
131+
Name: "encodedconfig.yaml",
132+
Encoding: "base64",
133+
Content: []byte(b64Config),
134+
}
135+
136+
data, err := yaml.Marshal(f)
137+
assert.NoError(t, err)
138+
139+
var out map[string]interface{}
140+
err = yaml.Unmarshal(data, &out)
141+
assert.NoError(t, err)
142+
assert.Equal(t, "encodedconfig.yaml", out["filename"])
143+
assert.Equal(t, "base64", out["encoding"])
144+
assert.Equal(t, b64Config, out["content"])
145+
}

0 commit comments

Comments
 (0)