1// Copyright 2019 Istio Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package util
16
17import (
18	"bytes"
19	"fmt"
20	"strings"
21
22	jsonpatch "github.com/evanphx/json-patch"
23	yaml2 "github.com/ghodss/yaml"
24	"github.com/gogo/protobuf/jsonpb"
25	"github.com/gogo/protobuf/proto"
26	jsonpb2 "github.com/golang/protobuf/jsonpb"
27	"github.com/kylelemons/godebug/diff"
28	"sigs.k8s.io/yaml"
29)
30
31// ToYAML returns a YAML string representation of val, or the error string if an error occurs.
32func ToYAML(val interface{}) string {
33	y, err := yaml2.Marshal(val)
34	if err != nil {
35		return err.Error()
36	}
37	return string(y)
38}
39
40// ToYAMLWithJSONPB returns a YAML string representation of val (using jsonpb), or the error string if an error occurs.
41func ToYAMLWithJSONPB(val proto.Message) string {
42	m := jsonpb.Marshaler{EnumsAsInts: true}
43	js, err := m.MarshalToString(val)
44	if err != nil {
45		return err.Error()
46	}
47	yb, err := yaml.JSONToYAML([]byte(js))
48	if err != nil {
49		return err.Error()
50	}
51	return string(yb)
52}
53
54// MarshalWithJSONPB returns a YAML string representation of val (using jsonpb).
55func MarshalWithJSONPB(val proto.Message) (string, error) {
56	m := jsonpb.Marshaler{EnumsAsInts: true}
57	js, err := m.MarshalToString(val)
58	if err != nil {
59		return "", err
60	}
61	yb, err := yaml.JSONToYAML([]byte(js))
62	if err != nil {
63		return "", err
64	}
65	return string(yb), nil
66}
67
68// UnmarshalWithJSONPB unmarshals y into out using gogo jsonpb (required for many proto defined structs).
69func UnmarshalWithJSONPB(y string, out proto.Message, allowUnknownField bool) error {
70	jb, err := yaml.YAMLToJSON([]byte(y))
71	if err != nil {
72		return err
73	}
74
75	u := jsonpb.Unmarshaler{AllowUnknownFields: allowUnknownField}
76	err = u.Unmarshal(bytes.NewReader(jb), out)
77	if err != nil {
78		return err
79	}
80	return nil
81}
82
83// UnmarshalValuesWithJSONPB unmarshals y into out using golang jsonpb.
84func UnmarshalValuesWithJSONPB(y string, out proto.Message, allowUnknown bool) error {
85	jb, err := yaml.YAMLToJSON([]byte(y))
86	if err != nil {
87		return err
88	}
89	u := jsonpb2.Unmarshaler{AllowUnknownFields: allowUnknown}
90	err = u.Unmarshal(bytes.NewReader(jb), out)
91	if err != nil {
92		return err
93	}
94	return nil
95}
96
97/*func ObjectsInManifest(mstr string) string {
98	ao, err := manifest.ParseObjectsFromYAMLManifest(mstr)
99	if err != nil {
100		return err.Error()
101	}
102	var out []string
103	for _, v := range ao {
104		out = append(out, v.Hash())
105	}
106	return strings.Join(out, "\n")
107}*/
108
109// OverlayTrees performs a sequential JSON strategic of overlays over base.
110func OverlayTrees(base map[string]interface{}, overlays ...map[string]interface{}) (map[string]interface{}, error) {
111	bby, err := yaml.Marshal(base)
112	if err != nil {
113		return nil, err
114	}
115	by := string(bby)
116
117	for _, o := range overlays {
118		oy, err := yaml.Marshal(o)
119		if err != nil {
120			return nil, err
121		}
122
123		by, err = OverlayYAML(by, string(oy))
124		if err != nil {
125			return nil, err
126		}
127	}
128
129	out := make(map[string]interface{})
130	err = yaml.Unmarshal([]byte(by), &out)
131	if err != nil {
132		return nil, err
133	}
134	return out, nil
135}
136
137// OverlayYAML patches the overlay tree over the base tree and returns the result. All trees are expressed as YAML
138// strings.
139func OverlayYAML(base, overlay string) (string, error) {
140	if strings.TrimSpace(base) == "" {
141		return overlay, nil
142	}
143	if strings.TrimSpace(overlay) == "" {
144		return base, nil
145	}
146	bj, err := yaml2.YAMLToJSON([]byte(base))
147	if err != nil {
148		return "", fmt.Errorf("yamlToJSON error in base: %s\n%s", err, bj)
149	}
150	oj, err := yaml2.YAMLToJSON([]byte(overlay))
151	if err != nil {
152		return "", fmt.Errorf("yamlToJSON error in overlay: %s\n%s", err, oj)
153	}
154	if base == "" {
155		bj = []byte("{}")
156	}
157	if overlay == "" {
158		oj = []byte("{}")
159	}
160
161	merged, err := jsonpatch.MergePatch(bj, oj)
162	if err != nil {
163		return "", fmt.Errorf("json merge error (%s) for base object: \n%s\n override object: \n%s", err, bj, oj)
164	}
165	my, err := yaml2.JSONToYAML(merged)
166	if err != nil {
167		return "", fmt.Errorf("jsonToYAML error (%s) for merged object: \n%s", err, merged)
168	}
169
170	return string(my), nil
171}
172
173func YAMLDiff(a, b string) string {
174	ao, bo := make(map[string]interface{}), make(map[string]interface{})
175	if err := yaml.Unmarshal([]byte(a), &ao); err != nil {
176		return err.Error()
177	}
178	if err := yaml.Unmarshal([]byte(b), &bo); err != nil {
179		return err.Error()
180	}
181
182	ay, err := yaml.Marshal(ao)
183	if err != nil {
184		return err.Error()
185	}
186	by, err := yaml.Marshal(bo)
187	if err != nil {
188		return err.Error()
189	}
190
191	return diff.Diff(string(ay), string(by))
192}
193
194// IsYAMLEqual reports whether the YAML in strings a and b are equal.
195func IsYAMLEqual(a, b string) bool {
196	if strings.TrimSpace(a) == "" && strings.TrimSpace(b) == "" {
197		return true
198	}
199	ajb, err := yaml.YAMLToJSON([]byte(a))
200	if err != nil {
201		scope.Debugf("bad YAML in isYAMLEqual:\n%s", a)
202		return false
203	}
204	bjb, err := yaml.YAMLToJSON([]byte(b))
205	if err != nil {
206		scope.Debugf("bad YAML in isYAMLEqual:\n%s", b)
207		return false
208	}
209
210	return string(ajb) == string(bjb)
211}
212
213// IsYAMLEmpty reports whether the YAML string y is logically empty.
214func IsYAMLEmpty(y string) bool {
215	var yc []string
216	for _, l := range strings.Split(y, "\n") {
217		yt := strings.TrimSpace(l)
218		if !strings.HasPrefix(yt, "#") && !strings.HasPrefix(yt, "---") {
219			yc = append(yc, l)
220		}
221	}
222	return strings.TrimSpace(strings.Join(yc, "\n")) == "{}"
223}
224