Skip to content

Commit

Permalink
✨ Generate schema for types with custom JSON marshaling as Any type
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Slavomir Kaslev committed May 5, 2020
1 parent 67819ee commit e0d7c9d
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 0 deletions.
19 changes: 19 additions & 0 deletions pkg/crd/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package crd
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"strings"

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

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

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

return typ, format, nil
}

// Open coded go/types representation of encoding/json.Marshaller
var jsonMarshaler = types.NewInterfaceType([]*types.Func{
types.NewFunc(token.NoPos, nil, "MarshalJSON",
types.NewSignature(nil, nil,
types.NewTuple(
types.NewVar(token.NoPos, nil, "", types.NewSlice(types.Universe.Lookup("byte").Type())),
types.NewVar(token.NoPos, nil, "", types.Universe.Lookup("error").Type())), false)),
}, nil).Complete()

func implementsJSONMarshaler(typ types.Type) bool {
return types.Implements(typ, jsonMarshaler) || types.Implements(types.NewPointer(typ), jsonMarshaler)
}
88 changes: 88 additions & 0 deletions pkg/crd/testdata/cronjob_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ limitations under the License.
package cronjob

import (
"encoding/json"
"fmt"
"net/url"

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

// +kubebuilder:validation:Type=string
// URL wraps url.URL.
// It has custom json marshal methods that enable it to be used in K8s CRDs
// such that the CRD resource will have the URL but operator code can can work with url.URL struct
type URL struct {
url.URL
}

func (u *URL) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%q", u.String())), nil
}

func (u *URL) UnmarshalJSON(b []byte) error {
var ref string
if err := json.Unmarshal(b, &ref); err != nil {
return err
}
if ref == "" {
*u = URL{}
return nil
}

r, err := url.Parse(ref)
if err != nil {
return err
} else if r != nil {
*u = URL{*r}
} else {
*u = URL{}
}
return nil
}

func (u *URL) String() string {
if u == nil {
return ""
}
return u.URL.String()
}

// +kubebuilder:validation:Type=string
// URL2 is an alias of url.URL.
// It has custom json marshal methods that enable it to be used in K8s CRDs
// such that the CRD resource will have the URL but operator code can can work with url.URL struct
type URL2 url.URL

func (u *URL2) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%q", u.String())), nil
}

func (u *URL2) UnmarshalJSON(b []byte) error {
var ref string
if err := json.Unmarshal(b, &ref); err != nil {
return err
}
if ref == "" {
*u = URL2{}
return nil
}

r, err := url.Parse(ref)
if err != nil {
return err
} else if r != nil {
*u = *(*URL2)(r)
} else {
*u = URL2{}
}
return nil
}

func (u *URL2) String() string {
if u == nil {
return ""
}
return (*url.URL)(u).String()
}

// ConcurrencyPolicy describes how the job will be handled.
// Only one of the following concurrent policies may be specified.
// If none of the following policies is specified, the default one
Expand Down Expand Up @@ -226,6 +306,14 @@ type CronJobStatus struct {
// with microsecond precision.
// +optional
LastScheduleMicroTime *metav1.MicroTime `json:"lastScheduleMicroTime,omitempty"`

// LastActiveLogURL specifies the logging url for the last started job
// +optional
LastActiveLogURL *URL `json:"lastActiveLogURL,omitempty"`

// LastActiveLogURL2 specifies the logging url for the last started job
// +optional
LastActiveLogURL2 *URL2 `json:"lastActiveLogURL2,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
8 changes: 8 additions & 0 deletions pkg/crd/testdata/testdata.kubebuilder.io_cronjobs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5167,6 +5167,14 @@ spec:
type: string
type: object
type: array
lastActiveLogURL:
description: LastActiveLogURL specifies the logging url for the last
started job
type: string
lastActiveLogURL2:
description: LastActiveLogURL2 specifies the logging url for the last
started job
type: string
lastScheduleMicroTime:
description: Information about the last time the job was successfully
scheduled, with microsecond precision.
Expand Down

0 comments on commit e0d7c9d

Please sign in to comment.