diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index e03757f4967..c3ea7e9b7b7 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -3,7 +3,9 @@ on: push: branches: - main - + paths: + - "docs/**" + jobs: deploy-site: runs-on: ubuntu-latest diff --git a/v2/go.mod b/v2/go.mod index e9165ca99fe..81f52be3724 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -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 diff --git a/v2/go.sum b/v2/go.sum index 182021204c1..5ea9562167c 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -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= diff --git a/v2/internal/reflecthelpers/reflect_helpers.go b/v2/internal/reflecthelpers/reflect_helpers.go index 7d7764600a7..847c7fc8fb6 100644 --- a/v2/internal/reflecthelpers/reflect_helpers.go +++ b/v2/internal/reflecthelpers/reflect_helpers.go @@ -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 +} diff --git a/v2/internal/reflecthelpers/reflect_helpers_test.go b/v2/internal/reflecthelpers/reflect_helpers_test.go index 373c6258a47..7606c9c46d7 100644 --- a/v2/internal/reflecthelpers/reflect_helpers_test.go +++ b/v2/internal/reflecthelpers/reflect_helpers_test.go @@ -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{} @@ -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) @@ -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)) +}