-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathnegotiator.go
More file actions
324 lines (285 loc) · 11.2 KB
/
negotiator.go
File metadata and controls
324 lines (285 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
package k8s
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/watch"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana-app-sdk/resource"
)
// GenericNegotiatedSerializer implements runtime.NegotiatedSerializer and allows for JSON serialization and
// deserialization of resource.Object. Since it is generic, and has no schema information,
// wrapped objects are returned which require a call to `Into` to marshal into an actual resource.Object.
type GenericNegotiatedSerializer struct {
}
// SupportedMediaTypes returns the JSON supported media type with a GenericJSONDecoder and kubernetes JSON Framer.
func (*GenericNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
return []runtime.SerializerInfo{{
MediaType: "application/json",
StreamSerializer: &runtime.StreamSerializerInfo{
Serializer: &GenericJSONDecoder{},
Framer: jsonserializer.Framer,
},
Serializer: &GenericJSONDecoder{},
}}
}
// EncoderForVersion returns the `serializer` input
func (*GenericNegotiatedSerializer) EncoderForVersion(serializer runtime.Encoder,
_ runtime.GroupVersioner) runtime.Encoder {
return serializer
}
// DecoderToVersion returns a GenericJSONDecoder
func (*GenericNegotiatedSerializer) DecoderToVersion(_ runtime.Decoder, _ runtime.GroupVersioner) runtime.Decoder {
return &GenericJSONDecoder{}
}
// GenericJSONDecoder implements runtime.Serializer and works with Untyped* objects to implement runtime.Object
type GenericJSONDecoder struct {
}
type objCheck struct {
metav1.TypeMeta `json:",inline"`
Type string `json:"type,omitempty"`
Items json.RawMessage `json:"items,omitempty"`
}
// Decode decodes the provided data into UntypedWatchObject or UntypedObjectWrapper
//
//nolint:gocritic,revive
func (*GenericJSONDecoder) Decode(
data []byte, defaults *schema.GroupVersionKind, into runtime.Object,
) (runtime.Object, *schema.GroupVersionKind, error) {
// Determine what kind of object we have the raw bytes for
// We do this by unmarshalling into a superset of a few possible types, then narrowing down
// TODO: this seems very naive, check how apimachinery does it typically
var chk objCheck
if err := json.Unmarshal(data, &chk); err != nil {
logging.DefaultLogger.Error("error unmarshalling into objCheck", "error", err)
return into, defaults, fmt.Errorf("error unmarshalling into objCheck: %w", err)
}
switch {
case chk.Type != "": // Watch
obj, err := unmarshalWithDefault(data, into, &metav1.WatchEvent{})
if err != nil {
logging.DefaultLogger.Error("error unmarshalling into *metav1.WatchEvent", "error", err)
return into, defaults, err
}
switch watch.EventType(obj.Type) {
case watch.Error, watch.Added, watch.Modified, watch.Deleted, watch.Bookmark:
// Other watch event types are already a resource.Object, so we can return them directly
return obj, defaults, nil
}
// If we get here, we have an unknown watch event type
logging.DefaultLogger.Error("unknown watch event type", "type", obj.Type)
return into, defaults, fmt.Errorf("unknown watch event type: %s", obj.Type)
case chk.APIVersion == StatusAPIVersion && chk.Kind == StatusKind: // Status
obj, err := unmarshalWithDefault(data, into, &metav1.Status{})
if err != nil {
logging.DefaultLogger.Error("error unmarshalling into *metav1.Status", "error", err)
return into, defaults, err
}
return obj, defaults, nil
case into != nil: // Other known Kind
if err := json.Unmarshal(data, into); err != nil {
logging.DefaultLogger.Error(
fmt.Sprintf("error unmarshalling into provided %T", into),
"error", err,
)
return into, defaults, err
}
return into, defaults, nil
case chk.Items != nil: // TODO: the codecs don't know how to handle lists yet.
return nil, nil, fmt.Errorf("unsupported list object")
case chk.Kind != "": // Fallback to UntypedObject
o := &UntypedObjectWrapper{}
if err := json.Unmarshal(data, o); err != nil {
logging.DefaultLogger.Error("error unmarshalling into *k8s.UntypedObjectWrapper", "error", err)
return into, defaults, fmt.Errorf("error unmarshalling into *k8s.UntypedObjectWrapper: %w", err)
}
o.object = data
into = o
}
return into, defaults, nil
}
// Encode json-encodes the provided object
func (*GenericJSONDecoder) Encode(obj runtime.Object, w io.Writer) error {
// TODO: check compliance with resource.Object and use marshalJSON in that case
b, e := json.Marshal(obj)
if e != nil {
return e
}
_, e = w.Write(b)
return e
}
// Identifier returns "generic-json-decoder"
func (*GenericJSONDecoder) Identifier() runtime.Identifier {
return "generic-json-decoder"
}
type KindNegotiatedSerializer struct {
Kind resource.Kind
}
// SupportedMediaTypes returns the JSON supported media type with a GenericJSONDecoder and kubernetes JSON Framer.
func (k *KindNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
supported := make([]runtime.SerializerInfo, 0, len(k.Kind.Codecs))
for encoding, codec := range k.Kind.Codecs {
serializer := &CodecDecoder{
SampleObject: k.Kind.ZeroValue(),
SampleList: k.Kind.ZeroListValue(),
Codec: codec,
}
info := runtime.SerializerInfo{
MediaType: string(encoding),
Serializer: serializer,
}
// Framer is used for the stream serializer
switch encoding {
case resource.KindEncodingJSON:
serializer.Decoder = json.Unmarshal
info.Serializer = serializer
info.StreamSerializer = &runtime.StreamSerializerInfo{
Serializer: serializer,
Framer: jsonserializer.Framer,
}
default:
// TODO: YAML framer
// case resource.KindEncodingYAML:
// framer = yamlserializer.Framer <- doesn't exist
}
supported = append(supported, info)
}
return supported
}
// EncoderForVersion returns the `serializer` input
func (*KindNegotiatedSerializer) EncoderForVersion(serializer runtime.Encoder,
_ runtime.GroupVersioner) runtime.Encoder {
return serializer
}
// DecoderToVersion returns a GenericJSONDecoder
func (*KindNegotiatedSerializer) DecoderToVersion(d runtime.Decoder, _ runtime.GroupVersioner) runtime.Decoder {
return d
}
// CodecDecoder implements runtime.Serializer and works with Untyped* objects to implement runtime.Object
type CodecDecoder struct {
SampleObject resource.Object
SampleList resource.ListObject
Codec resource.Codec
Decoder func([]byte, any) error
}
type indicator struct {
metav1.TypeMeta `json:",inline"`
Items *noAlloc `json:"items,omitempty"`
}
// noAlloc is used to avoid allocating any memory when unmarshaling, it can be used as a signal field
type noAlloc struct {
}
func (*noAlloc) UnmarshalJSON([]byte) error {
return nil
}
// Decode decodes the provided data into UntypedWatchObject or UntypedObjectWrapper
//
//nolint:gocritic,revive
func (c *CodecDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (
runtime.Object, *schema.GroupVersionKind, error) {
if into != nil {
switch cast := into.(type) {
case resource.Object:
logging.DefaultLogger.Debug("decoding object into provided resource.Object", "gvk", into.GetObjectKind().GroupVersionKind().String())
err := c.Codec.Read(bytes.NewReader(data), cast)
return cast, defaults, err
case resource.ListObject:
logging.DefaultLogger.Debug("decoding object into provided resource.ListObject", "gvk", into.GetObjectKind().GroupVersionKind().String())
// TODO: use codec for each element in the list?
err := c.Decoder(data, cast)
return cast, defaults, err
case *metav1.WatchEvent:
logging.DefaultLogger.Debug("decoding object into provided *v1.WatchEvent", "gvk", into.GetObjectKind().GroupVersionKind().String())
err := c.Decoder(data, cast)
return cast, defaults, err
case *metav1.List:
logging.DefaultLogger.Debug("decoding object into provided *v1.List", "gvk", into.GetObjectKind().GroupVersionKind().String())
err := c.Decoder(data, cast)
return cast, defaults, err
case *metav1.Status:
logging.DefaultLogger.Debug("decoding object into provided *v1.Status", "gvk", into.GetObjectKind().GroupVersionKind().String())
err := c.Decoder(data, cast)
return cast, defaults, err
}
// TODO: This is the same process (just without casting) as WatchEvent, List, and Status (they all use the default Decoder). Should we still keep them separate?
logging.DefaultLogger.Debug("decoding object into provided unregistered resource using default Decoder", "gvk", into.GetObjectKind().GroupVersionKind().String())
err := c.Decoder(data, into)
return into, defaults, err
}
if defaults != nil {
if defaults.Kind == "Status" && defaults.Version == "v1" {
logging.DefaultLogger.Debug("decoding object into *v1.Status resource based on defaults", "gvk", defaults.String())
obj := &metav1.Status{}
err := c.Decoder(data, obj)
return obj, defaults, err
}
logging.DefaultLogger.Debug("defaults present", "gvk", defaults.String())
}
tm := indicator{}
err := c.Decoder(data, &tm)
if err != nil {
return nil, nil, fmt.Errorf("error decoding object TypeMeta: %w", err)
}
if tm.GroupVersionKind().Version == "v1" && tm.GroupVersionKind().Kind == "Status" {
logging.DefaultLogger.Debug("decoding object into *v1.Status resource based on decoded TypeMeta", "gvk", tm.GroupVersionKind().String())
obj := &metav1.Status{}
err := c.Decoder(data, obj)
return obj, defaults, err
}
// Check if this is a List
if tm.Items != nil {
logging.DefaultLogger.Debug("decoding into a new empty list instance from kind", "gvk", tm.GroupVersionKind().String())
var obj resource.ListObject
if c.SampleList != nil {
obj = c.SampleList.Copy()
} else {
logging.DefaultLogger.Warn("no SampleObject set in CodecDecoder, using *resource.TypedList[*resource.UntypedObject]")
obj = &resource.TypedList[*resource.UntypedObject]{}
}
// TODO: use codec for each element in the list?
err = c.Decoder(data, &obj)
return obj, defaults, err
}
// Default to the data being the kind this CodecDecoder is for
logging.DefaultLogger.Debug("decoding into a new empty object instance from kind", "gvk", tm.GroupVersionKind().String())
var obj resource.Object
if c.SampleObject != nil {
obj = c.SampleObject.Copy()
} else {
logging.DefaultLogger.Warn("no SampleObject set in CodecDecoder, using *resource.UntypedObject")
obj = &resource.UntypedObject{}
}
err = c.Codec.Read(bytes.NewReader(data), obj)
return obj, defaults, err
}
// Encode json-encodes the provided object
func (c *CodecDecoder) Encode(obj runtime.Object, w io.Writer) error {
if cast, ok := obj.(resource.Object); ok {
return c.Codec.Write(w, cast)
}
return errors.New("provided object is not a resource.Object")
}
// Identifier returns "generic-json-decoder"
func (*CodecDecoder) Identifier() runtime.Identifier {
return "codec-decoder"
}
func unmarshalWithDefault[T any](data []byte, obj runtime.Object, defVal T) (T, error) {
res := defVal
if obj != nil {
cast, ok := obj.(T)
if !ok {
return res, fmt.Errorf("unable to cast %T into %T", obj, res)
}
res = cast
}
if err := json.Unmarshal(data, res); err != nil {
return defVal, err
}
return res, nil
}