Skip to content

Commit

Permalink
Merge branch 'main' into feature/sb-test-race-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
theunrepentantgeek authored Oct 3, 2023
2 parents 7de8ed5 + fa59534 commit 318af3a
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 5 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/deploy-site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ on:
push:
branches:
- main

paths:
- "docs/**"

jobs:
deploy-site:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion v2/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ require (
github.com/jackc/pgx/v5 v5.4.3
github.com/kr/pretty v0.3.1
github.com/kylelemons/godebug v1.1.0
github.com/leanovate/gopter v0.2.8
github.com/leanovate/gopter v0.2.9
github.com/onsi/gomega v1.28.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.17.0
Expand Down
4 changes: 2 additions & 2 deletions v2/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leanovate/gopter v0.2.8 h1:eFPtJ3aa5zLfbxGROSNY75T9Dume60CWBAqoWQ3h/ig=
github.com/leanovate/gopter v0.2.8/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8=
github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c=
github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
Expand Down
93 changes: 93 additions & 0 deletions v2/internal/reflecthelpers/reflect_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,96 @@ func getItemsField(listPtr client.ObjectList) (reflect.Value, error) {

return itemsField, nil
}

// SetProperty sets the property on the provided object to the provided value.
// obj is the object to modify.
// propertyPath is a dot-separated path to the property to set.
// value is the value to set the property to.
// Returns an error if any of the properties in the path do not exist, if the property is not settable,
// or if the value provided is incompatible.
func SetProperty(obj any, propertyPath string, value any) error {
if obj == nil {
return errors.Errorf("provided object was nil")
}

if propertyPath == "" {
return errors.Errorf("property path was empty")
}

steps := strings.Split(propertyPath, ".")
return setPropertyCore(obj, steps, value)
}

func setPropertyCore(obj any, propertyPath []string, value any) (err error) {
// Catch any panic that occurs when setting the field and turn it into an error return
defer func() {
if recovered := recover(); recovered != nil {
err = errors.Errorf("failed to set property %s: %s", propertyPath[0], recovered)
}
}()

// Get the underlying object we need to modify
subject := reflect.ValueOf(obj)

// Dereference pointers
if subject.Kind() == reflect.Ptr {
subject = subject.Elem()
}

// Check we have a struct
if subject.Kind() != reflect.Struct {
return errors.Errorf("provided object was not a struct, was %s", subject.Kind())
}

// Get the field we need to modify
field := subject.FieldByName(propertyPath[0])

// Check the field exists
if field == (reflect.Value{}) {
return errors.Errorf("provided object did not have a field named %s", propertyPath[0])
}

// If this is not the last property in the path, we need to recurse
if len(propertyPath) > 1 {
if field.Kind() == reflect.Ptr {
// Field is a pointer; initialize it if needed, then pass the pointer recursively
if field.IsNil() {
newValue := reflect.New(field.Type().Elem())
field.Set(newValue)
}

err = setPropertyCore(field.Interface(), propertyPath[1:], value)
if err != nil {
return errors.Wrapf(err, "failed to set property %s",
propertyPath[0])
}

return nil
}

// Field is not a pointer, so we need to pass the address of the field recursively
err = setPropertyCore(field.Addr().Interface(), propertyPath[1:], value)
if err != nil {
return errors.Wrapf(err, "failed to set property %s",
propertyPath[0])
}

return nil
}

// If this is the last property in the path, we need to set the value, if we can
if !field.CanSet() {
return errors.Errorf("field %s was not settable", propertyPath[0])
}

// Cast value to the type required by the field
valueKind := reflect.ValueOf(value)
if !valueKind.CanConvert(field.Type()) {
return errors.Errorf("value of kind %s was not compatible with field %s", valueKind, propertyPath[0])
}

value = valueKind.Convert(field.Type()).Interface()

field.Set(reflect.ValueOf(value))
return nil
}
183 changes: 182 additions & 1 deletion v2/internal/reflecthelpers/reflect_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import (
type ResourceWithReferences struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ResourceWithReferencesSpec `json:"spec,omitempty"`
Spec ResourceWithReferencesSpec `json:"spec,omitempty"`
Status ResourceWithReferencesStatus `json:"status,omitempty"`
}

var _ client.Object = &ResourceWithReferences{}
Expand Down Expand Up @@ -118,10 +119,21 @@ func (in *ResourceWithReferencesSpec) PopulateFromARM(owner genruntime.Arbitrary
panic("not expected to be called")
}

type ResourceWithReferencesStatus struct {
ProvisioningState *ProvisioningState `json:"provisioningState,omitempty"`
}

type ResourceReference struct {
Reference genruntime.ResourceReference `armReference:"Id" json:"reference"`
}

type ProvisioningState string

const (
ProvisioningStateSucceeded ProvisioningState = "Succeeded"
ProvisioningStateFailed ProvisioningState = "Failed"
)

func Test_FindReferences(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)
Expand Down Expand Up @@ -408,3 +420,172 @@ func Test_SetObjectListItems(t *testing.T) {
g.Expect(list.Items).To(HaveLen(1))
g.Expect(list.Items[0].GetName()).To(Equal("test-group"))
}

func Test_SetProperty_TargetingStringProperty_MakesChange(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

subject := &ResourceWithReferencesSpec{
AzureName: "azureName",
}

newValue := "newName"
err := reflecthelpers.SetProperty(subject, "AzureName", newValue)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(subject.AzureName).To(Equal(newValue))
}

func Test_SetProperty_TargetingMultipleProperties_MakesChanges(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

subject := &ResourceWithReferencesSpec{
AzureName: "azureName",
Location: "westus",
PropertyWithTag: to.Ptr("hello"),
}

name := "dorothy"
location := "land-of-oz"
property := to.Ptr("flying-house")

g.Expect(reflecthelpers.SetProperty(subject, "AzureName", name)).To(Succeed())
g.Expect(reflecthelpers.SetProperty(subject, "Location", location)).To(Succeed())
g.Expect(reflecthelpers.SetProperty(subject, "PropertyWithTag", property)).To(Succeed())

g.Expect(subject.AzureName).To(Equal(name))
g.Expect(subject.Location).To(Equal(location))
g.Expect(subject.PropertyWithTag).To(Equal(property))
}

func Test_SetProperty_TargetingNestedStringProperty_MakesChange(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

subject := &ResourceWithReferences{
Spec: ResourceWithReferencesSpec{
AzureName: "azureName",
},
}

newValue := "newName"
err := reflecthelpers.SetProperty(subject, "Spec.AzureName", newValue)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(subject.Spec.AzureName).To(Equal(newValue))
}

func Test_SetProperty_TargetingMultipleNestedProperties_MakesChange(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

subject := &ResourceWithReferences{
Spec: ResourceWithReferencesSpec{
AzureName: "azureName",
Location: "westus",
PropertyWithTag: to.Ptr("hello"),
},
}

name := "dorothy"
location := "land-of-oz"
property := to.Ptr("flying-house")

g.Expect(reflecthelpers.SetProperty(subject, "Spec.AzureName", name)).To(Succeed())
g.Expect(reflecthelpers.SetProperty(subject, "Spec.Location", location)).To(Succeed())
g.Expect(reflecthelpers.SetProperty(subject, "Spec.PropertyWithTag", property)).To(Succeed())

g.Expect(subject.Spec.AzureName).To(Equal(name))
g.Expect(subject.Spec.Location).To(Equal(location))
g.Expect(subject.Spec.PropertyWithTag).To(Equal(property))
}

func Test_SetProperty_TargetingNestedNestedStringProperty_MakesChange(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

subject := &ResourceWithReferences{
Spec: ResourceWithReferencesSpec{
AzureName: "azureName",
},
}

newValue := "newName"
err := reflecthelpers.SetProperty(subject, "Spec.Owner.Name", newValue)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(subject.Spec.Owner.Name).To(Equal(newValue))
}

func Test_SetProperty_TargetingMultipleNestedNestedProperties_MakesChanges(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

subject := &ResourceWithReferences{}

name := "lock"
key := "key"

g.Expect(reflecthelpers.SetProperty(subject, "Spec.Secret.Name", name)).To(Succeed())
g.Expect(reflecthelpers.SetProperty(subject, "Spec.Secret.Key", key)).To(Succeed())

g.Expect(subject.Spec.Secret.Name).To(Equal(name))
g.Expect(subject.Spec.Secret.Key).To(Equal(key))
}

func Test_SetProperty_TargetingUnknownProperty_ReturnsExpectedError(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

subject := &ResourceWithReferencesSpec{
AzureName: "azureName",
}

err := reflecthelpers.SetProperty(subject, "UnknownProperty", "newValue")
g.Expect(err).To(HaveOccurred())
g.Expect(err).To(MatchError(ContainSubstring("UnknownProperty")))
}

func Test_SetProperty_TargetingUnknownNestedProperty_ReturnsExpectedError(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

subject := &ResourceWithReferencesSpec{}

err := reflecthelpers.SetProperty(subject, "Owner.UnknownProperty", "newValue")
g.Expect(err).To(HaveOccurred())

g.Expect(err).To(MatchError(ContainSubstring("Owner")))
g.Expect(err).To(MatchError(ContainSubstring("UnknownProperty")))
}

func Test_SetProperty_WhenValueOfWrongType_ReturnsExpectedError(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

subject := &ResourceWithReferences{
Spec: ResourceWithReferencesSpec{
AzureName: "azureName",
},
}

err := reflecthelpers.SetProperty(subject, "Spec.AzureName", make([]int, 0, 10))
g.Expect(err).To(HaveOccurred())
g.Expect(err).To(MatchError(ContainSubstring("Spec")))
g.Expect(err).To(MatchError(ContainSubstring("AzureName")))
g.Expect(err).To(MatchError(ContainSubstring("kind []")))
g.Expect(err).To(MatchError(ContainSubstring("not compatible")))
}

func Test_SetProperty_WhenValueOfCompatibleType_ModifiesValue(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

subject := &ResourceWithReferences{
Status: ResourceWithReferencesStatus{
ProvisioningState: to.Ptr(ProvisioningStateSucceeded),
},
}

err := reflecthelpers.SetProperty(subject, "Status.ProvisioningState", to.Ptr(string(ProvisioningStateFailed)))
g.Expect(err).ToNot(HaveOccurred())
g.Expect(*subject.Status.ProvisioningState).To(Equal(ProvisioningStateFailed))
}

0 comments on commit 318af3a

Please sign in to comment.