Skip to content

Commit

Permalink
Merge pull request #29 from ezgidemirel/add-set-name
Browse files Browse the repository at this point in the history
Add a helper function "setResourceNameAnnotation"
  • Loading branch information
ezgidemirel authored Nov 16, 2023
2 parents cfda8ce + 7a23c01 commit b9145ee
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 34 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,12 @@ about `crossplane beta render`.
## Additional functions

| Name | Description |
| ---------------------------------------------------------------- | --------------------------------------------------- |
|------------------------------------------------------------------| --------------------------------------------------- |
| [`randomChoice`](example/functions/randomChoice) | Randomly selects one of a given strings |
| [`toYaml`](example/functions/toYaml) | Marshals any object into a YAML string |
| [`fromYaml`](example/functions/fromYaml) | Unmarshals a YAML string into an object |
| [`getResourceCondition`](example/functions/getResourceCondition) | Helper function to retreive conditions of resources |
| [`setResourceNameAnnotation`](example/functions/inline) | Returns the special resource-name annotation with given name |

## Developing this function

Expand Down
2 changes: 1 addition & 1 deletion example/filesystem/templates.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ apiVersion: iam.aws.upbound.io/v1beta1
kind: AccessKey
metadata:
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: sample-access-key-{{ $i }}
gotemplating.fn.crossplane.io/composition-resource-name: sample-access-key-{{ $i }}
spec:
forProvider:
userSelector:
Expand Down
4 changes: 2 additions & 2 deletions example/inline/composition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ spec:
kind: User
metadata:
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: test-user-{{ $i }}
{{ setResourceNameAnnotation (print "test-user-" $i) }}
labels:
testing.upbound.io/example-name: test-user-{{ $i }}
{{ if eq $.observed.resources nil }}
Expand All @@ -38,7 +38,7 @@ spec:
kind: AccessKey
metadata:
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: sample-access-key-{{ $i }}
{{ setResourceNameAnnotation (print "sample-access-key-" $i) }}
spec:
forProvider:
userSelector:
Expand Down
72 changes: 42 additions & 30 deletions function_maps.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"math/rand"
"text/template"
"time"
Expand All @@ -15,36 +16,11 @@ import (

var funcMaps = []template.FuncMap{
{
"randomChoice": func(choices ...string) string {
r := rand.New(rand.NewSource(time.Now().UnixNano()))

return choices[r.Intn(len(choices))]
},

"toYaml": func(val any) (string, error) {
res, err := yaml.Marshal(val)
if err != nil {
return "", err
}
return string(res), nil
},

"fromYaml": func(val string) (any, error) {
var res any
err := yaml.Unmarshal([]byte(val), &res)
return res, err
},

"getResourceCondition": func(ct string, res map[string]any) (xpv1.Condition, error) {
var conditioned xpv1.ConditionedStatus
if err := fieldpath.Pave(res).GetValueInto("resource.status", &conditioned); err != nil {
conditioned = xpv1.ConditionedStatus{}
}

// Return either found condition or empty one with "Unknown" status
cond := conditioned.GetCondition(xpv1.ConditionType(ct))
return cond, nil
},
"randomChoice": randomChoice,
"toYaml": toYaml,
"fromYaml": fromYaml,
"getResourceCondition": getResourceCondition,
"setResourceNameAnnotation": setResourceNameAnnotation,
},
}

Expand All @@ -58,3 +34,39 @@ func GetNewTemplateWithFunctionMaps() *template.Template {

return tpl
}

func randomChoice(choices ...string) string {
r := rand.New(rand.NewSource(time.Now().UnixNano()))

return choices[r.Intn(len(choices))]
}

func toYaml(val any) (string, error) {
res, err := yaml.Marshal(val)
if err != nil {
return "", err
}

return string(res), nil
}

func fromYaml(val string) (any, error) {
var res any
err := yaml.Unmarshal([]byte(val), &res)

return res, err
}

func getResourceCondition(ct string, res map[string]any) xpv1.Condition {
var conditioned xpv1.ConditionedStatus
if err := fieldpath.Pave(res).GetValueInto("resource.status", &conditioned); err != nil {
conditioned = xpv1.ConditionedStatus{}
}

// Return either found condition or empty one with "Unknown" status
return conditioned.GetCondition(xpv1.ConditionType(ct))
}

func setResourceNameAnnotation(name string) string {
return fmt.Sprintf("gotemplating.fn.crossplane.io/composition-resource-name: %s", name)
}
248 changes: 248 additions & 0 deletions function_maps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package main

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/protobuf/testing/protocmp"

v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
)

func Test_fromYaml(t *testing.T) {
type args struct {
val string
}
type want struct {
rsp any
err error
}
cases := map[string]struct {
reason string
args args
want want
}{
"UnmarshalYaml": {
reason: "Should return unmarshalled yaml",
args: args{
val: `
complexDictionary:
scalar1: true
list:
- abc
- def`,
},
want: want{
rsp: map[string]interface{}{
"complexDictionary": map[string]interface{}{
"scalar1": true,
"list": []interface{}{
"abc",
"def",
},
},
},
},
},
"UnmarshalYamlError": {
reason: "Should return error when unmarshalling yaml",
args: args{
val: `
complexDictionary:
scalar1: true
`,
},
want: want{
err: cmpopts.AnyError,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
rsp, err := fromYaml(tc.args.val)

if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" {
t.Errorf("%s\nfromYaml(...): -want rsp, +got rsp:\n%s", tc.reason, diff)
}

if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {
t.Errorf("%s\nfromYaml(...): -want err, +got err:\n%s", tc.reason, diff)
}
})
}
}

func Test_toYaml(t *testing.T) {
type args struct {
val any
}
type want struct {
rsp any
err error
}
cases := map[string]struct {
reason string
args args
want want
}{
"MarshalYaml": {
reason: "Should return marshalled yaml",
args: args{
val: map[string]interface{}{
"complexDictionary": map[string]interface{}{
"scalar1": true,
"list": []interface{}{
"abc",
"def",
},
},
},
},
want: want{
rsp: `complexDictionary:
list:
- abc
- def
scalar1: true
`,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
rsp, err := toYaml(tc.args.val)

if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" {
t.Errorf("%s\ntoYaml(...): -want rsp, +got rsp:\n%s", tc.reason, diff)
}

if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {
t.Errorf("%s\ntoYaml(...): -want err, +got err:\n%s", tc.reason, diff)
}
})
}
}

func Test_getResourceCondition(t *testing.T) {
type args struct {
ct string
res map[string]any
}

type want struct {
rsp v1.Condition
}
cases := map[string]struct {
reason string
args args
want want
}{
"GetCondition": {
reason: "Should return condition",
args: args{
ct: "Ready",
res: map[string]any{
"resource": map[string]any{
"status": map[string]any{
"conditions": []any{
map[string]any{
"type": "Ready",
"status": "True",
},
},
},
},
},
},
want: want{
rsp: v1.Condition{
Type: "Ready",
Status: "True",
},
},
},
"GetConditionUnknown": {
reason: "Should return an Unknown status",
args: args{
ct: "Ready",
res: map[string]any{
"resource": map[string]any{},
},
},
want: want{
rsp: v1.Condition{
Type: "Ready",
Status: "Unknown",
},
},
},
"GetConditionNotFound": {
reason: "Should return an Unknown condition when not found",
args: args{
ct: "Ready",
res: map[string]any{
"resource": map[string]any{
"status": map[string]any{
"conditions": []any{
map[string]any{
"type": "NotReady",
"status": "True",
},
},
},
},
},
},
want: want{
rsp: v1.Condition{
Type: "Ready",
Status: "Unknown",
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
rsp := getResourceCondition(tc.args.ct, tc.args.res)

if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" {
t.Errorf("%s\ngetResourceCondition(...): -want rsp, +got rsp:\n%s", tc.reason, diff)
}
})
}
}

func Test_setResourceNameAnnotation(t *testing.T) {
type args struct {
name string
}
type want struct {
rsp string
}

cases := map[string]struct {
reason string
args args
want want
}{
"SetAnnotationWithGivenName": {
reason: "Should return composition resource name annotation with given name",
args: args{
name: "test",
},
want: want{
rsp: "gotemplating.fn.crossplane.io/composition-resource-name: test",
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
rsp := setResourceNameAnnotation(tc.args.name)

if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" {
t.Errorf("%s\nsetResourceNameAnnotation(...): -want rsp, +got rsp:\n%s", tc.reason, diff)
}
})
}
}

0 comments on commit b9145ee

Please sign in to comment.