Skip to content

Commit ee59f32

Browse files
alvaroalemanSlavomir Kaslev
and
Slavomir Kaslev
authored
🐛 Allow setting the type for structs that implement json.Marshaler (#601)
* ✨ Generate schema for types with custom JSON marshaling as Any type Currently controller-gen complains about structs with missing json tags even when those structs implement custom JSON marshalling. With this change we check if a type implements custom JSON marshalling and if it does, we output schema for Any type. This still allows the validation type to be overriden with a marker. * 🏃 Regenerate pkg/crd/testdata testdata.kubebuilder.io_cronjobs.yaml * 🐛 Allow overriding type of types with custom unmarshaler This slightly extends the initial work in PR 427 to fall back to the standard generation rather than producing a schema with Any in it, as that is invalid and led us to revert that PR. Co-authored-by: Slavomir Kaslev <[email protected]>
1 parent 03ab6b3 commit ee59f32

File tree

3 files changed

+4376
-946
lines changed

3 files changed

+4376
-946
lines changed

pkg/crd/schema.go

+23
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package crd
1919
import (
2020
"fmt"
2121
"go/ast"
22+
"go/token"
2223
"go/types"
2324
"strings"
2425

@@ -109,6 +110,15 @@ func (c *schemaContext) requestSchema(pkgPath, typeName string) {
109110

110111
// infoToSchema creates a schema for the type in the given set of type information.
111112
func infoToSchema(ctx *schemaContext) *apiext.JSONSchemaProps {
113+
// If the obj implements a JSON marshaler and has a marker, use the markers value and do not traverse as
114+
// the marshaler could be doing anything. If there is no marker, fall back to traversing.
115+
if obj := ctx.pkg.Types.Scope().Lookup(ctx.info.Name); obj != nil && implementsJSONMarshaler(obj.Type()) {
116+
schema := &apiext.JSONSchemaProps{}
117+
applyMarkers(ctx, ctx.info.Markers, schema, ctx.info.RawSpec.Type)
118+
if schema.Type != "" {
119+
return schema
120+
}
121+
}
112122
return typeToSchema(ctx, ctx.info.RawSpec.Type)
113123
}
114124

@@ -431,3 +441,16 @@ func builtinToType(basic *types.Basic, allowDangerousTypes bool) (typ string, fo
431441

432442
return typ, format, nil
433443
}
444+
445+
// Open coded go/types representation of encoding/json.Marshaller
446+
var jsonMarshaler = types.NewInterfaceType([]*types.Func{
447+
types.NewFunc(token.NoPos, nil, "MarshalJSON",
448+
types.NewSignature(nil, nil,
449+
types.NewTuple(
450+
types.NewVar(token.NoPos, nil, "", types.NewSlice(types.Universe.Lookup("byte").Type())),
451+
types.NewVar(token.NoPos, nil, "", types.Universe.Lookup("error").Type())), false)),
452+
}, nil).Complete()
453+
454+
func implementsJSONMarshaler(typ types.Type) bool {
455+
return types.Implements(typ, jsonMarshaler) || types.Implements(types.NewPointer(typ), jsonMarshaler)
456+
}

pkg/crd/testdata/cronjob_types.go

+104
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ package cronjob
2525
import (
2626
"encoding/json"
2727
"fmt"
28+
"net/url"
29+
"time"
2830

2931
batchv1beta1 "k8s.io/api/batch/v1beta1"
3032
corev1 "k8s.io/api/core/v1"
@@ -247,6 +249,98 @@ func (t *TotallyABool) UnmarshalJSON(in []byte) error {
247249
return nil
248250
}
249251

252+
// +kubebuilder:validation:Type=string
253+
// URL wraps url.URL.
254+
// It has custom json marshal methods that enable it to be used in K8s CRDs
255+
// such that the CRD resource will have the URL but operator code can can work with url.URL struct
256+
type URL struct {
257+
url.URL
258+
}
259+
260+
func (u *URL) MarshalJSON() ([]byte, error) {
261+
return []byte(fmt.Sprintf("%q", u.String())), nil
262+
}
263+
264+
func (u *URL) UnmarshalJSON(b []byte) error {
265+
var ref string
266+
if err := json.Unmarshal(b, &ref); err != nil {
267+
return err
268+
}
269+
if ref == "" {
270+
*u = URL{}
271+
return nil
272+
}
273+
274+
r, err := url.Parse(ref)
275+
if err != nil {
276+
return err
277+
} else if r != nil {
278+
*u = URL{*r}
279+
} else {
280+
*u = URL{}
281+
}
282+
return nil
283+
}
284+
285+
func (u *URL) String() string {
286+
if u == nil {
287+
return ""
288+
}
289+
return u.URL.String()
290+
}
291+
292+
// +kubebuilder:validation:Type=string
293+
// URL2 is an alias of url.URL.
294+
// It has custom json marshal methods that enable it to be used in K8s CRDs
295+
// such that the CRD resource will have the URL but operator code can can work with url.URL struct
296+
type URL2 url.URL
297+
298+
func (u *URL2) MarshalJSON() ([]byte, error) {
299+
return []byte(fmt.Sprintf("%q", u.String())), nil
300+
}
301+
302+
func (u *URL2) UnmarshalJSON(b []byte) error {
303+
var ref string
304+
if err := json.Unmarshal(b, &ref); err != nil {
305+
return err
306+
}
307+
if ref == "" {
308+
*u = URL2{}
309+
return nil
310+
}
311+
312+
r, err := url.Parse(ref)
313+
if err != nil {
314+
return err
315+
} else if r != nil {
316+
*u = *(*URL2)(r)
317+
} else {
318+
*u = URL2{}
319+
}
320+
return nil
321+
}
322+
323+
func (u *URL2) String() string {
324+
if u == nil {
325+
return ""
326+
}
327+
return (*url.URL)(u).String()
328+
}
329+
330+
// Duration has a custom Marshaler but no markers.
331+
// We want the CRD generation to infer type information
332+
// from the go types and ignore the presense of the Marshaler.
333+
type Duration struct {
334+
Value time.Duration `json:"value"`
335+
}
336+
337+
func (d Duration) MarshalJSON() ([]byte, error) {
338+
type durationWithoutUnmarshaler Duration
339+
return json.Marshal(durationWithoutUnmarshaler(d))
340+
}
341+
342+
var _ json.Marshaler = Duration{}
343+
250344
// ConcurrencyPolicy describes how the job will be handled.
251345
// Only one of the following concurrent policies may be specified.
252346
// If none of the following policies is specified, the default one
@@ -283,6 +377,16 @@ type CronJobStatus struct {
283377
// with microsecond precision.
284378
// +optional
285379
LastScheduleMicroTime *metav1.MicroTime `json:"lastScheduleMicroTime,omitempty"`
380+
381+
// LastActiveLogURL specifies the logging url for the last started job
382+
// +optional
383+
LastActiveLogURL *URL `json:"lastActiveLogURL,omitempty"`
384+
385+
// LastActiveLogURL2 specifies the logging url for the last started job
386+
// +optional
387+
LastActiveLogURL2 *URL2 `json:"lastActiveLogURL2,omitempty"`
388+
389+
Runtime *Duration `json:"duration,omitempty"`
286390
}
287391

288392
// +kubebuilder:object:root=true

0 commit comments

Comments
 (0)