Skip to content

Commit

Permalink
Add config.Resource.ServerSideApplyListMapKeys to be able to configur…
Browse files Browse the repository at this point in the history
…e the list map keys

for merging items of object lists via server-side apply patches.

Signed-off-by: Alper Rifat Ulucinar <[email protected]>
  • Loading branch information
ulucinar committed Dec 5, 2023
1 parent ca2cfe6 commit 2175ab1
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 17 deletions.
23 changes: 12 additions & 11 deletions pkg/config/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
38 changes: 38 additions & 0 deletions pkg/config/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
45 changes: 44 additions & 1 deletion pkg/types/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
47 changes: 42 additions & 5 deletions pkg/types/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions pkg/types/markers/kubebuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type KubebuilderOptions struct {
Required *bool
Minimum *int
Maximum *int
Default *string
}

func (o KubebuilderOptions) String() string {
Expand All @@ -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
}

0 comments on commit 2175ab1

Please sign in to comment.