Skip to content

Commit 3753ca3

Browse files
authored
Introducing a generator configuration for GoTag customization (#485)
This patch introduces a new configuration, `go_tag`, to allow ACK developers to customize the "Go tags" in the generated structures. With this configuration users can easily override the default Go tags, making adjustements like changing the tag name, adding omitempty directives or even introducing new ones (yaml, gorm, protobuf etc). This change helps ACK users overcome the challenge imposed by our names utility package (`aws-controllers-k8s/pkg/names/`) which adds an underscore suffix to field names that matches Golang keywords. Resulting in an odd UX when consuming the generated ACK CRDs (e.g using `type_: <...>` instead of `type: <...>`) We purposefully avoided directly modifying the `names` utility package (trimming the underscores when generating the go tags) as it will break compatibility with the already existing CRDs (yep we missed this). Without some sort of mutating webhooks that will handle field renames, we'll avoid going that route, and instead start using `go_tag` configs for future CRDs/fields. This patch will help in tweaking the generated code for aws-controllers-k8s/eks-controller#84 Signed-off-by: Amine Hilaly <[email protected]> By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 87c16d7 commit 3753ca3

File tree

5 files changed

+107
-2
lines changed

5 files changed

+107
-2
lines changed

pkg/config/field.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,30 @@ type FieldConfig struct {
405405
// TODO(jaypipes,crtbry): Figure out if we can roll the CustomShape stuff
406406
// into this type override...
407407
Type *string `json:"type,omitempty"`
408+
// GoTag is used to override the default Go tag injected into the fields of
409+
// a generated go structure. This is useful if we want to override the json
410+
// tag name or add an omitempty directive to the tag. If not specified,
411+
// the default json tag is used, i.e. json:"<fieldName>,omitempty"
412+
//
413+
// The main reason behind introducing this feature is that, our naming utility
414+
// package appends an underscore suffix to the field name if it is colliding with
415+
// a Golang keyword (switch, if, else etc...). This is needed to avoid violating
416+
// the Go language spec, when defining package names, variable names, etc.
417+
// This functionality resulted in injecting the underscore suffix to the json tag
418+
// as well, e.g. json:"type_,omitempty". Which is not ideal because it weirdens
419+
// the experience for the users of the generated CRDs.
420+
//
421+
// One could argue that we should just modify the `names`` package to return an
422+
// extra field indicating whether the field name is a Go keyword or not, or even
423+
// better, return the correct go tag dirrctly. The reason why we should avoid
424+
// such a change is that it would modify the already existing/generated code, which
425+
// would break the compatibility for the existing CRDs. Without introducing some
426+
// sort of mutating webhook to handle field name change, this is not a viable.
427+
// We decided to introduce this feature to, at least, allow us to override the
428+
// go tag for any new resource or fields that we generate in the future.
429+
//
430+
// (See https://github.com/aws-controllers-k8s/pkg/blob/main/names/names.go)
431+
GoTag *string `json:"go_tag,omitempty"`
408432
}
409433

410434
// GetFieldConfigs returns all FieldConfigs for a given resource as a map.

pkg/model/field.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,27 @@ func (f *Field) GetFieldDocsConfig() *ackgenconfig.FieldDocsConfig {
216216
return resourceConfig.Fields[f.Path]
217217
}
218218

219+
// GetGoTag returns the json tag for the field. If the field has a
220+
// FieldConfig with a GoTag attribute, the value of GoTag is returned.
221+
// Otherwise, we evaluate the field type and return the appropriate
222+
// json tag.
223+
func (f *Field) GetGoTag() string {
224+
// First check if the field has a GoTag attribute in the FieldConfig
225+
// a.k.a generator.yaml
226+
if f.FieldConfig != nil && f.FieldConfig.GoTag != nil {
227+
return fmt.Sprintf("`%s`", *f.FieldConfig.GoTag)
228+
}
229+
230+
// If the field is not required, a reference field or part of the status
231+
// object, we need to inject the `omitempty`` directive into the json tag.
232+
if !f.IsRequired() || f.HasReference() || f.CRD.StatusFields[f.Names.Camel] != nil {
233+
return fmt.Sprintf("`json:\"%s,omitempty\"`", f.Names.CamelLower)
234+
235+
}
236+
237+
return fmt.Sprintf("`json:\"%s\"`", f.Names.CamelLower)
238+
}
239+
219240
// HasReference returns true if the supplied field *path* refers to a Field
220241
// that contains 'ReferencesConfig' i.e. has a corresponding reference field.
221242
// Ex:

pkg/model/field_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,59 @@ func TestGetReferenceFieldName(t *testing.T) {
254254
}
255255
}
256256

257+
func TestGetGoTag(t *testing.T) {
258+
require := require.New(t)
259+
assert := assert.New(t)
260+
g := testutil.NewModelForServiceWithOptions(t, "eks",
261+
&testutil.TestingModelOptions{
262+
GeneratorConfigFile: "generator-with-gotag.yaml",
263+
},
264+
)
265+
266+
crds, err := g.GetCRDs()
267+
require.Nil(err)
268+
269+
crd := getCRDByName("Cluster", crds)
270+
require.NotNil(crd)
271+
272+
testCases := []struct {
273+
name string
274+
field *model.Field
275+
expectedTag string
276+
}{
277+
{
278+
name: "not required spec field",
279+
field: crd.SpecFields["Logging"],
280+
expectedTag: "`json:\"logging,omitempty\"`",
281+
},
282+
{
283+
name: "required spec field",
284+
field: crd.SpecFields["Name"],
285+
expectedTag: "`json:\"name\"`",
286+
},
287+
{
288+
name: "spec field with go_tag override",
289+
field: crd.SpecFields["Version"],
290+
expectedTag: "`json:\"myCustomVersionName,omitempty\"`",
291+
},
292+
{
293+
// Status fields are not required, so they should always have omitempty directive
294+
name: "status field",
295+
field: crd.StatusFields["Endpoint"],
296+
expectedTag: "`json:\"endpoint,omitempty\"`",
297+
},
298+
{
299+
name: "status field with go_tag override",
300+
field: crd.StatusFields["Status"],
301+
expectedTag: "`json:\"clusterState,omitempty\" yaml:\"some_extra_tags\"`",
302+
},
303+
}
304+
for _, tc := range testCases {
305+
tag := tc.field.GetGoTag()
306+
assert.Equal(tc.expectedTag, tag)
307+
}
308+
}
309+
257310
func TestReferenceFieldPath(t *testing.T) {
258311
assert := assert.New(t)
259312

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
resources:
2+
Cluster:
3+
fields:
4+
Version:
5+
go_tag: json:"myCustomVersionName,omitempty"
6+
Status:
7+
go_tag: json:"clusterState,omitempty" yaml:"some_extra_tags"

templates/apis/crd.go.tpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type {{ .CRD.Kind }}Spec struct {
2222
{{- if and ($field.IsRequired) (not $field.HasReference) -}}
2323
// +kubebuilder:validation:Required
2424
{{ end -}}
25-
{{ $field.Names.Camel }} {{ $field.GoType }} `json:"{{ $field.Names.CamelLower }}{{- if or (not $field.IsRequired) ($field.HasReference) }},omitempty{{- end -}}"`
25+
{{ $field.Names.Camel }} {{ $field.GoType }} {{ $field.GetGoTag }}
2626
{{- end }}
2727
}
2828

@@ -44,7 +44,7 @@ type {{ .CRD.Kind }}Status struct {
4444
{{ $field.GetDocumentation }}
4545
{{- end }}
4646
// +kubebuilder:validation:Optional
47-
{{ $field.Names.Camel }} {{ $field.GoType }} `json:"{{ $field.Names.CamelLower }},omitempty"`
47+
{{ $field.Names.Camel }} {{ $field.GoType }} {{ $field.GetGoTag }}
4848
{{- end }}
4949
}
5050

0 commit comments

Comments
 (0)