Skip to content

Commit e0d7c9d

Browse files
author
Slavomir Kaslev
committed
✨ 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.
1 parent 67819ee commit e0d7c9d

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed

pkg/crd/schema.go

+19
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

@@ -104,6 +105,11 @@ func (c *schemaContext) requestSchema(pkgPath, typeName string) {
104105

105106
// infoToSchema creates a schema for the type in the given set of type information.
106107
func infoToSchema(ctx *schemaContext) *apiext.JSONSchemaProps {
108+
if obj := ctx.pkg.Types.Scope().Lookup(ctx.info.Name); obj != nil && implementsJSONMarshaler(obj.Type()) {
109+
schema := &apiext.JSONSchemaProps{Type: "Any"}
110+
applyMarkers(ctx, ctx.info.Markers, schema, ctx.info.RawSpec.Type)
111+
return schema
112+
}
107113
return typeToSchema(ctx, ctx.info.RawSpec.Type)
108114
}
109115

@@ -419,3 +425,16 @@ func builtinToType(basic *types.Basic) (typ string, format string, err error) {
419425

420426
return typ, format, nil
421427
}
428+
429+
// Open coded go/types representation of encoding/json.Marshaller
430+
var jsonMarshaler = types.NewInterfaceType([]*types.Func{
431+
types.NewFunc(token.NoPos, nil, "MarshalJSON",
432+
types.NewSignature(nil, nil,
433+
types.NewTuple(
434+
types.NewVar(token.NoPos, nil, "", types.NewSlice(types.Universe.Lookup("byte").Type())),
435+
types.NewVar(token.NoPos, nil, "", types.Universe.Lookup("error").Type())), false)),
436+
}, nil).Complete()
437+
438+
func implementsJSONMarshaler(typ types.Type) bool {
439+
return types.Implements(typ, jsonMarshaler) || types.Implements(types.NewPointer(typ), jsonMarshaler)
440+
}

pkg/crd/testdata/cronjob_types.go

+88
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ limitations under the License.
2323
package cronjob
2424

2525
import (
26+
"encoding/json"
2627
"fmt"
28+
"net/url"
2729

2830
batchv1beta1 "k8s.io/api/batch/v1beta1"
2931
corev1 "k8s.io/api/core/v1"
@@ -190,6 +192,84 @@ func (t *TotallyABool) UnmarshalJSON(in []byte) error {
190192
return nil
191193
}
192194

195+
// +kubebuilder:validation:Type=string
196+
// URL wraps url.URL.
197+
// It has custom json marshal methods that enable it to be used in K8s CRDs
198+
// such that the CRD resource will have the URL but operator code can can work with url.URL struct
199+
type URL struct {
200+
url.URL
201+
}
202+
203+
func (u *URL) MarshalJSON() ([]byte, error) {
204+
return []byte(fmt.Sprintf("%q", u.String())), nil
205+
}
206+
207+
func (u *URL) UnmarshalJSON(b []byte) error {
208+
var ref string
209+
if err := json.Unmarshal(b, &ref); err != nil {
210+
return err
211+
}
212+
if ref == "" {
213+
*u = URL{}
214+
return nil
215+
}
216+
217+
r, err := url.Parse(ref)
218+
if err != nil {
219+
return err
220+
} else if r != nil {
221+
*u = URL{*r}
222+
} else {
223+
*u = URL{}
224+
}
225+
return nil
226+
}
227+
228+
func (u *URL) String() string {
229+
if u == nil {
230+
return ""
231+
}
232+
return u.URL.String()
233+
}
234+
235+
// +kubebuilder:validation:Type=string
236+
// URL2 is an alias of url.URL.
237+
// It has custom json marshal methods that enable it to be used in K8s CRDs
238+
// such that the CRD resource will have the URL but operator code can can work with url.URL struct
239+
type URL2 url.URL
240+
241+
func (u *URL2) MarshalJSON() ([]byte, error) {
242+
return []byte(fmt.Sprintf("%q", u.String())), nil
243+
}
244+
245+
func (u *URL2) UnmarshalJSON(b []byte) error {
246+
var ref string
247+
if err := json.Unmarshal(b, &ref); err != nil {
248+
return err
249+
}
250+
if ref == "" {
251+
*u = URL2{}
252+
return nil
253+
}
254+
255+
r, err := url.Parse(ref)
256+
if err != nil {
257+
return err
258+
} else if r != nil {
259+
*u = *(*URL2)(r)
260+
} else {
261+
*u = URL2{}
262+
}
263+
return nil
264+
}
265+
266+
func (u *URL2) String() string {
267+
if u == nil {
268+
return ""
269+
}
270+
return (*url.URL)(u).String()
271+
}
272+
193273
// ConcurrencyPolicy describes how the job will be handled.
194274
// Only one of the following concurrent policies may be specified.
195275
// If none of the following policies is specified, the default one
@@ -226,6 +306,14 @@ type CronJobStatus struct {
226306
// with microsecond precision.
227307
// +optional
228308
LastScheduleMicroTime *metav1.MicroTime `json:"lastScheduleMicroTime,omitempty"`
309+
310+
// LastActiveLogURL specifies the logging url for the last started job
311+
// +optional
312+
LastActiveLogURL *URL `json:"lastActiveLogURL,omitempty"`
313+
314+
// LastActiveLogURL2 specifies the logging url for the last started job
315+
// +optional
316+
LastActiveLogURL2 *URL2 `json:"lastActiveLogURL2,omitempty"`
229317
}
230318

231319
// +kubebuilder:object:root=true

pkg/crd/testdata/testdata.kubebuilder.io_cronjobs.yaml

+8
Original file line numberDiff line numberDiff line change
@@ -5167,6 +5167,14 @@ spec:
51675167
type: string
51685168
type: object
51695169
type: array
5170+
lastActiveLogURL:
5171+
description: LastActiveLogURL specifies the logging url for the last
5172+
started job
5173+
type: string
5174+
lastActiveLogURL2:
5175+
description: LastActiveLogURL2 specifies the logging url for the last
5176+
started job
5177+
type: string
51705178
lastScheduleMicroTime:
51715179
description: Information about the last time the job was successfully
51725180
scheduled, with microsecond precision.

0 commit comments

Comments
 (0)