diff --git a/response/response.go b/response/response.go index 170d863..91ac50f 100644 --- a/response/response.go +++ b/response/response.go @@ -21,6 +21,7 @@ import ( "encoding/json" "time" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" @@ -37,13 +38,23 @@ const DefaultTTL = 1 * time.Minute // To bootstraps a response to the supplied request. It automatically copies the // desired state from the request. func To(req *v1.RunFunctionRequest, ttl time.Duration) *v1.RunFunctionResponse { + var desired *v1.State + if d := req.GetDesired(); d != nil { + desired = proto.Clone(d).(*v1.State) + } + + var ctx *structpb.Struct + if c := req.GetContext(); c != nil { + ctx = proto.Clone(c).(*structpb.Struct) + } + return &v1.RunFunctionResponse{ Meta: &v1.ResponseMeta{ Tag: req.GetMeta().GetTag(), Ttl: durationpb.New(ttl), }, - Desired: req.GetDesired(), - Context: req.GetContext(), + Desired: desired, + Context: ctx, } } diff --git a/response/response_test.go b/response/response_test.go index 2187072..b87bd14 100644 --- a/response/response_test.go +++ b/response/response_test.go @@ -19,16 +19,57 @@ package response import ( "encoding/json" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" v1 "github.com/crossplane/function-sdk-go/proto/v1" "github.com/crossplane/function-sdk-go/resource" ) +func TestToClonesRequestState(t *testing.T) { + req := &v1.RunFunctionRequest{ + Meta: &v1.RequestMeta{Tag: "original"}, + Desired: &v1.State{ + Resources: map[string]*v1.Resource{ + "existing": { + Resource: resource.MustStructJSON(`{ + "apiVersion": "example.org/v1", + "kind": "Widget" + }`), + }, + }, + }, + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "existing": structpb.NewStringValue("value"), + }, + }, + } + + rsp := To(req, time.Minute) + + SetContextKey(rsp, "new", structpb.NewStringValue("response-only")) + rsp.Desired.Resources["response-only"] = &v1.Resource{ + Resource: resource.MustStructJSON(`{ + "apiVersion": "example.org/v1", + "kind": "ExtraWidget" + }`), + } + + if got, ok := req.GetContext().GetFields()["new"]; ok { + t.Fatalf("To(...) aliased request context: unexpected field %q", got.GetStringValue()) + } + + if _, ok := req.GetDesired().GetResources()["response-only"]; ok { + t.Fatal("To(...) aliased desired resources from the request") + } +} + func TestSetDesiredResources(t *testing.T) { type args struct { rsp *v1.RunFunctionResponse