Skip to content

Commit

Permalink
Merge pull request #301 from negz/scribble
Browse files Browse the repository at this point in the history
Add server-side apply merge strategy markers
  • Loading branch information
ulucinar authored Dec 5, 2023
2 parents bed1fa2 + 5150500 commit ca2cfe6
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 1 deletion.
32 changes: 32 additions & 0 deletions pkg/types/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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"
)

Expand Down Expand Up @@ -152,9 +153,40 @@ func NewField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema,
f.FieldType = fieldType
f.InitType = initType

if !sch.Sensitive {
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 sets.
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"

// ListTypeMap 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 ca2cfe6

Please sign in to comment.