From 2175ab15556a04578ff65f3a820943da0362f7ce Mon Sep 17 00:00:00 2001 From: Alper Rifat Ulucinar Date: Fri, 1 Dec 2023 03:01:30 +0300 Subject: [PATCH] Add config.Resource.ServerSideApplyListMapKeys to be able to configure the list map keys for merging items of object lists via server-side apply patches. Signed-off-by: Alper Rifat Ulucinar --- pkg/config/common.go | 23 ++++++++-------- pkg/config/resource.go | 38 ++++++++++++++++++++++++++ pkg/types/builder.go | 45 +++++++++++++++++++++++++++++- pkg/types/field.go | 47 ++++++++++++++++++++++++++++---- pkg/types/markers/kubebuilder.go | 4 +++ 5 files changed, 140 insertions(+), 17 deletions(-) diff --git a/pkg/config/common.go b/pkg/config/common.go index ee291e39..4fbd34e5 100644 --- a/pkg/config/common.go +++ b/pkg/config/common.go @@ -77,17 +77,18 @@ func DefaultResource(name string, terraformSchema *schema.Resource, terraformReg } r := &Resource{ - Name: name, - TerraformResource: terraformSchema, - MetaResource: terraformRegistry, - ShortGroup: group, - Kind: kind, - Version: "v1alpha1", - ExternalName: NameAsIdentifier, - References: map[string]Reference{}, - Sensitive: NopSensitive, - UseAsync: true, - SchemaElementOptions: make(map[string]*SchemaElementOption), + Name: name, + TerraformResource: terraformSchema, + MetaResource: terraformRegistry, + ShortGroup: group, + Kind: kind, + Version: "v1alpha1", + ExternalName: NameAsIdentifier, + References: make(References), + Sensitive: NopSensitive, + UseAsync: true, + SchemaElementOptions: make(SchemaElementOptions), + ServerSideApplyListMapKeys: make(ServerSideApplyListMapKeys), } for _, f := range opts { f(r) diff --git a/pkg/config/resource.go b/pkg/config/resource.go index 876ec6f5..bdff429b 100644 --- a/pkg/config/resource.go +++ b/pkg/config/resource.go @@ -266,6 +266,30 @@ func setExternalTagsWithPaved(externalTags map[string]string, paved *fieldpath.P return pavedByte, nil } +type InjectedKey struct { + Key string + DefaultValue string +} + +type ListMapKeys struct { + InjectedKey InjectedKey + Keys []string +} + +// ServerSideApplyListMapKeys configures the generated field at the specified +// path (a map key) as an object list for server-side apply patch operations, +// sets the server-side apply merge strategy for the object list to map, +// and finally sets the list map keys to the corresponding map value. +// The specified field (via the Terraform argument path) must be a list +// of objects and the Terraform configuration argument names specified +// in the map value must be scalars. If a non-zero `InjectedKey` is +// specified, then a field of type string with the specified name is injected +// into the Terraform schema and used as a list map key together with +// any other existing keys specified in `Keys`. +// Please also see: +// https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy +type ServerSideApplyListMapKeys map[string]ListMapKeys + // Resource is the set of information that you can override at different steps // of the code generation pipeline. type Resource struct { @@ -338,6 +362,20 @@ type Resource struct { // Terraform InstanceDiff is computed during reconciliation. TerraformCustomDiff CustomDiff + // ServerSideApplyListMapKeys configures the list map keys for the keyed + // object lists. The value must be a set of scalar Terraform argument + // names to be used as the list map keys for the object list. The key is + // a Terraform configuration argument path such as a.b.c, without any + // index notation (i.e., array/map components do not need indices). + // The list map keys are leaf Terraform argument names. An example + // configuration would map a.b.c to [d, e], where a.b.c is the + // Terraform argument canonical path for the object list c, and + // the scalar Terraform configuration arguments a.b.c.d and a.b.c.e + // constitute the list map keys. The object list a.b.c will have a + // server-side apply merge strategy of `map` with the specified + // list map keys. + ServerSideApplyListMapKeys ServerSideApplyListMapKeys + // useNoForkClient indicates that a no-fork external client should // be generated instead of the Terraform CLI-forking client. useNoForkClient bool diff --git a/pkg/types/builder.go b/pkg/types/builder.go index b4dbc3ef..2df265e0 100644 --- a/pkg/types/builder.go +++ b/pkg/types/builder.go @@ -27,6 +27,9 @@ const ( // ref: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation-rules celEscapeSequence = "__%s__" + // description for an injected list map key field in the context of the + // server-side apply object list merging + descriptionInjectedKey = "This is an injected field with a default value for being able to merge items of the parent object list." ) var ( @@ -67,6 +70,10 @@ func NewBuilder(pkg *types.Package) *Builder { // Build returns parameters and observation types built out of Terraform schema. func (g *Builder) Build(cfg *config.Resource) (Generated, error) { + if err := injectServerSideApplyMergeKeys(cfg); err != nil { + return Generated{}, errors.Wrapf(err, "cannot inject server-side apply merge keys for resource %q", cfg.Name) + } + fp, ap, ip, err := g.buildResource(cfg.TerraformResource, cfg, nil, nil, false, cfg.Kind) return Generated{ Types: g.genTypes, @@ -75,7 +82,43 @@ func (g *Builder) Build(cfg *config.Resource) (Generated, error) { InitProviderType: ip, AtProviderType: ap, ValidationRules: g.validationRules, - }, errors.Wrapf(err, "cannot build the Types") + }, errors.Wrapf(err, "cannot build the Types for resource %q", cfg.Name) +} + +func injectServerSideApplyMergeKeys(cfg *config.Resource) error { + for f, lmk := range cfg.ServerSideApplyListMapKeys { + if lmk.InjectedKey.Key == "" && len(lmk.Keys) == 0 { + return errors.Errorf("list map keys configuration for the object list %q is empty", f) + } + if lmk.InjectedKey.Key == "" { + continue + } + sch := config.GetSchema(cfg.TerraformResource, f) + if sch == nil { + return errors.Errorf("cannot find the Terraform schema for the argument at the path %q", f) + } + if sch.Type != schema.TypeList && sch.Type != schema.TypeSet { + return errors.Errorf("fieldpath %q is not a Terraform list or set", f) + } + el, ok := sch.Elem.(*schema.Resource) + if !ok { + return errors.Errorf("fieldpath %q is a Terraform list or set but its element type is not a Terraform *schema.Resource", f) + } + for k := range el.Schema { + if k == lmk.InjectedKey.Key { + return errors.Errorf("element schema for the object list %q already contains the argument key %q", f, k) + } + } + el.Schema[lmk.InjectedKey.Key] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: descriptionInjectedKey, + } + if lmk.InjectedKey.DefaultValue != "" { + el.Schema[lmk.InjectedKey.Key].Default = lmk.InjectedKey.DefaultValue + } + } + return nil } func (g *Builder) buildResource(res *schema.Resource, cfg *config.Resource, tfPath []string, xpPath []string, asBlocksMode bool, names ...string) (*types.Named, *types.Named, *types.Named, error) { //nolint:gocyclo diff --git a/pkg/types/field.go b/pkg/types/field.go index 81ce87fd..4259de6b 100644 --- a/pkg/types/field.go +++ b/pkg/types/field.go @@ -40,6 +40,9 @@ type Field struct { TransformedName string SelectorName string Identifier bool + // Injected is set if this Field is an injected field to the Terraform + // schema as an object list map key for server-side apply merges. + Injected bool } // getDocString tries to extract the documentation string for the specified @@ -156,6 +159,7 @@ func NewField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema, if !sch.Sensitive { AddServerSideApplyMarkers(f) } + AddServerSideApplyMarkersFromConfig(f, cfg) return f, nil } @@ -187,6 +191,32 @@ func AddServerSideApplyMarkers(f *Field) { // objects with a well-known key that we could merge on? } +func AddServerSideApplyMarkersFromConfig(f *Field, cfg *config.Resource) { + fp := strings.ReplaceAll(strings.Join(f.TerraformPaths, "."), ".*.", ".") + fp = strings.TrimSuffix(fp, ".*") + for k, mapKeys := range cfg.ServerSideApplyListMapKeys { + if fp == fmt.Sprintf("%s.%s", k, mapKeys.InjectedKey.Key) { + if mapKeys.InjectedKey.DefaultValue != "" { + f.Comment.KubebuilderOptions.Default = ptr.To[string](mapKeys.InjectedKey.DefaultValue) + } + f.TFTag = "-" // prevent serialization into Terraform configuration + f.Injected = true + continue + } + if k != fp { + continue + } + f.Comment.ServerSideApplyOptions.ListType = ptr.To[markers.ListType](markers.ListTypeMap) + f.Comment.ServerSideApplyOptions.ListMapKey = make([]string, 0, len(mapKeys.Keys)+1) + for _, k := range mapKeys.Keys { + f.Comment.ServerSideApplyOptions.ListMapKey = append(f.Comment.ServerSideApplyOptions.ListMapKey, k) + } + if mapKeys.InjectedKey.Key != "" { + f.Comment.ServerSideApplyOptions.ListMapKey = append(f.Comment.ServerSideApplyOptions.ListMapKey, mapKeys.InjectedKey.Key) + } + } +} + // NewSensitiveField returns a constructed sensitive Field object. func NewSensitiveField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema, snakeFieldName string, tfPath, xpPath, names []string, asBlocksMode bool) (*Field, bool, error) { //nolint:gocyclo f, err := NewField(g, cfg, r, sch, snakeFieldName, tfPath, xpPath, names, asBlocksMode) @@ -267,9 +297,15 @@ func (f *Field) AddToResource(g *Builder, r *resource, typeNames *TypeNames, add // parameter field if it's not an observation (only) field, i.e. parameter. // // We do this only if tf tag is not set to "-" because otherwise it won't - // be populated from the tfstate. We typically set tf tag to "-" for - // sensitive fields which were replaced with secretKeyRefs. - if f.TFTag != "-" && !addToObservation { + // be populated from the tfstate. Injected fields are included in the + // observation because an associative-list in the spec should also be + // an associative-list in the observation (status). + // We also make sure that this field has not already been added to the + // observation type via an explicit resource configuration. + // We typically set tf tag to "-" for sensitive fields which were replaced + // with secretKeyRefs, or for injected fields into the CRD schema, + // which do not exist in the Terraform schema. + if (f.TFTag != "-" || f.Injected) && !addToObservation { r.addObservationField(f, field) } @@ -313,7 +349,8 @@ func (f *Field) AddToResource(g *Builder, r *resource, typeNames *TypeNames, add // isInit returns true if the field should be added to initProvider. // We don't add Identifiers, references or fields which tag is set to -// "-". +// "-" unless they are injected object list map keys for server-side apply +// merges. // // Identifiers as they should not be ignorable or part of init due // the fact being created for one identifier and then updated for another @@ -327,7 +364,7 @@ func (f *Field) AddToResource(g *Builder, r *resource, typeNames *TypeNames, add // an earlier step, so they cannot be included as well. Plus probably they // should also not change for Create and Update steps. func (f *Field) isInit() bool { - return !f.Identifier && f.Reference == nil && f.TFTag != "-" + return !f.Identifier && f.Reference == nil && (f.TFTag != "-" || f.Injected) } func getDescription(s string) string { diff --git a/pkg/types/markers/kubebuilder.go b/pkg/types/markers/kubebuilder.go index 6b1c6e15..e1b386a2 100644 --- a/pkg/types/markers/kubebuilder.go +++ b/pkg/types/markers/kubebuilder.go @@ -12,6 +12,7 @@ type KubebuilderOptions struct { Required *bool Minimum *int Maximum *int + Default *string } func (o KubebuilderOptions) String() string { @@ -30,6 +31,9 @@ func (o KubebuilderOptions) String() string { if o.Maximum != nil { m += fmt.Sprintf("+kubebuilder:validation:Maximum=%d\n", *o.Maximum) } + if o.Default != nil { + m += fmt.Sprintf("+kubebuilder:default:=%s\n", *o.Default) + } return m }