Skip to content

Commit

Permalink
Add server-side apply merge strategy markers
Browse files Browse the repository at this point in the history
This commit adds SSA merge strategy markers that allow the API server to
granularly merge lists, rather than atomically replacing them. Composition
functions use SSA, so this change means that a function can return only the list
elements it desires (e.g. tags) and those elements will be merged into any
existing elements, without replacing them.

For the moment I've only covered two cases:

* Lists that we know are sets of scalar values (generated from Terraform sets)
* Maps of scalar values (generated from Terraform maps)

I'm hopeful that in both of these cases it _should_ be possible to allow the map
or set to be granularly merged, not atomically replaced.

https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy

Signed-off-by: Nic Cope <[email protected]>
  • Loading branch information
negz committed Nov 12, 2023
1 parent c4a76d2 commit 8ef1aa2
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 1 deletion.
30 changes: 30 additions & 0 deletions pkg/types/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/crossplane/upjet/pkg"
"github.com/crossplane/upjet/pkg/config"
"github.com/crossplane/upjet/pkg/types/comments"
"github.com/crossplane/upjet/pkg/types/markers"
"github.com/crossplane/upjet/pkg/types/name"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/pkg/errors"
Expand Down Expand Up @@ -151,9 +152,38 @@ func NewField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema,
f.FieldType = fieldType
f.InitType = initType

AddServerSideApplyMarkers(f)

return f, nil
}

// AddServerSideApplyMarkers adds server-side apply comment markers to indicate
// that scalar maps and sets can be merged granularly, not replace atomically.
func AddServerSideApplyMarkers(f *Field) {
switch f.Schema.Type { //nolint:exhaustive
case schema.TypeMap:
// A map should always have an element of type Schema.
if es, ok := f.Schema.Elem.(*schema.Schema); ok {
switch es.Type { //nolint:exhaustive
// We assume scalar types can be granular maps.
case schema.TypeString, schema.TypeBool, schema.TypeInt, schema.TypeFloat:
f.Comment.ServerSideApplyOptions.MapType = ptr.To[markers.MapType](markers.MapTypeGranular)
}
}
case schema.TypeSet:
if es, ok := f.Schema.Elem.(*schema.Schema); ok {
switch es.Type { //nolint:exhaustive
// We assume scalar types can be granular maps.
case schema.TypeString, schema.TypeBool, schema.TypeInt, schema.TypeFloat:
f.Comment.ServerSideApplyOptions.ListType = ptr.To[markers.ListType](markers.ListTypeSet)
}
}
}
// TODO(negz): Can we reliably add SSA markers for lists of objects? Do we
// have cases where we're turning a Terraform map of maps into a list of
// objects with a well-known key that we could merge on?
}

// 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
4 changes: 3 additions & 1 deletion pkg/types/markers/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ type Options struct {
UpjetOptions
CrossplaneOptions
KubebuilderOptions
ServerSideApplyOptions
}

// String returns a string representation of this Options object.
func (o Options) String() string {
return o.UpjetOptions.String() +
o.CrossplaneOptions.String() +
o.KubebuilderOptions.String()
o.KubebuilderOptions.String() +
o.ServerSideApplyOptions.String()
}
87 changes: 87 additions & 0 deletions pkg/types/markers/ssa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
//
// SPDX-License-Identifier: Apache-2.0

package markers

import "fmt"

// A ListType is a type of list.
type ListType string

// Types of lists.
const (
// ListTypeAtomic means the entire list is replaced during merge. At any
// point in time, a single manager owns the list.
ListTypeAtomic ListType = "atomic"

// ListTypeSet can be granularly merged, and different managers can own
// different elements in the list. The list can include only scalar
// elements.
ListTypeSet ListType = "set"

// ListTypeSet can be granularly merged, and different managers can own
// different elements in the list. The list can include only nested types
// (i.e. objects).
ListTypeMap ListType = "map"
)

// A MapType is a type of map.
type MapType string

// Types of maps.
const (
// MapTypeAtomic means that the map can only be entirely replaced by a
// single manager.
MapTypeAtomic MapType = "atomic"

// MapTypeGranular means that the map supports separate managers updating
// individual fields.
MapTypeGranular MapType = "granular"
)

// A StructType is a type of struct.
type StructType string

// Struct types.
const (
// StructTypeAtomic means that the struct can only be entirely replaced by a
// single manager.
StructTypeAtomic StructType = "atomic"

// StructTypeGranular means that the struct supports separate managers
// updating individual fields.
StructTypeGranular StructType = "granular"
)

// ServerSideApplyOptions represents the server-side apply merge options that
// upjet needs to control.
// https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy
type ServerSideApplyOptions struct {
ListType *ListType
ListMapKey []string
MapType *MapType
StructType *StructType
}

func (o ServerSideApplyOptions) String() string {
m := ""

if o.ListType != nil {
m += fmt.Sprintf("+listType:%s\n", *o.ListType)
}

for _, k := range o.ListMapKey {
m += fmt.Sprintf("+listMapKey:%s\n", k)
}

if o.MapType != nil {
m += fmt.Sprintf("+mapType:%s\n", *o.MapType)
}

if o.StructType != nil {
m += fmt.Sprintf("+structType:%s\n", *o.StructType)
}

return m
}
49 changes: 49 additions & 0 deletions pkg/types/markers/ssa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
//
// SPDX-License-Identifier: Apache-2.0

package markers

import (
"testing"

"github.com/google/go-cmp/cmp"
"k8s.io/utils/ptr"
)

func TestServerSideApplyOptions(t *testing.T) {
cases := map[string]struct {
o ServerSideApplyOptions
want string
}{
"MapType": {
o: ServerSideApplyOptions{
MapType: ptr.To[MapType](MapTypeAtomic),
},
want: "+mapType:atomic\n",
},
"StructType": {
o: ServerSideApplyOptions{
StructType: ptr.To[StructType](StructTypeAtomic),
},
want: "+structType:atomic\n",
},
"ListType": {
o: ServerSideApplyOptions{
ListType: ptr.To[ListType](ListTypeMap),
ListMapKey: []string{"name", "coolness"},
},
want: "+listType:map\n+listMapKey:name\n+listMapKey:coolness\n",
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := tc.o.String()

if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("o.String(): -want, +got: %s", diff)
}
})
}
}

0 comments on commit 8ef1aa2

Please sign in to comment.