diff --git a/.changes/unreleased/FEATURES-20241028-164944.yaml b/.changes/unreleased/FEATURES-20241028-164944.yaml new file mode 100644 index 0000000..f7428df --- /dev/null +++ b/.changes/unreleased/FEATURES-20241028-164944.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'all: Upgrade protocol versions to support ephemeral resource types' +time: 2024-10-28T16:49:44.46102-04:00 +custom: + Issue: "257" diff --git a/go.mod b/go.mod index ded2c9f..20f2ff7 100644 --- a/go.mod +++ b/go.mod @@ -6,29 +6,29 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.24.0 + github.com/hashicorp/terraform-plugin-go v0.25.0 github.com/hashicorp/terraform-plugin-log v0.9.0 - google.golang.org/grpc v1.66.2 + google.golang.org/grpc v1.67.1 ) require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/go-plugin v1.6.1 // indirect + github.com/hashicorp/go-plugin v1.6.2 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/oklog/run v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect - google.golang.org/protobuf v1.34.2 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/protobuf v1.35.1 // indirect ) diff --git a/go.sum b/go.sum index 3fe9c73..99e7874 100644 --- a/go.sum +++ b/go.sum @@ -11,12 +11,12 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= -github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= +github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= +github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.24.0 h1:2WpHhginCdVhFIrWHxDEg6RBn3YaWzR2o6qUeIEat2U= -github.com/hashicorp/terraform-plugin-go v0.24.0/go.mod h1:tUQ53lAsOyYSckFGEefGC5C8BAaO0ENqzFd3bQeuYQg= +github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks= +github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -31,8 +31,9 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= @@ -40,29 +41,31 @@ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= -google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/tf5testserver/tf5testserver.go b/internal/tf5testserver/tf5testserver.go index 1f810fa..83b11de 100644 --- a/internal/tf5testserver/tf5testserver.go +++ b/internal/tf5testserver/tf5testserver.go @@ -18,6 +18,8 @@ type TestServer struct { CallFunctionCalled map[string]bool + CloseEphemeralResourceCalled map[string]bool + ConfigureProviderCalled bool ConfigureProviderResponse *tfprotov5.ConfigureProviderResponse @@ -34,6 +36,8 @@ type TestServer struct { MoveResourceStateCalled map[string]bool + OpenEphemeralResourceCalled map[string]bool + PlanResourceChangeCalled map[string]bool PrepareProviderConfigCalled bool @@ -43,11 +47,15 @@ type TestServer struct { ReadResourceCalled map[string]bool + RenewEphemeralResourceCalled map[string]bool + StopProviderCalled bool StopProviderResponse *tfprotov5.StopProviderResponse UpgradeResourceStateCalled map[string]bool + ValidateEphemeralResourceConfigCalled map[string]bool + ValidateDataSourceConfigCalled map[string]bool ValidateResourceTypeConfigCalled map[string]bool @@ -75,6 +83,15 @@ func (s *TestServer) CallFunction(_ context.Context, req *tfprotov5.CallFunction return nil, nil } +func (s *TestServer) CloseEphemeralResource(ctx context.Context, req *tfprotov5.CloseEphemeralResourceRequest) (*tfprotov5.CloseEphemeralResourceResponse, error) { + if s.CloseEphemeralResourceCalled == nil { + s.CloseEphemeralResourceCalled = make(map[string]bool) + } + + s.CloseEphemeralResourceCalled[req.TypeName] = true + return nil, nil +} + func (s *TestServer) ConfigureProvider(_ context.Context, _ *tfprotov5.ConfigureProviderRequest) (*tfprotov5.ConfigureProviderResponse, error) { s.ConfigureProviderCalled = true @@ -137,6 +154,15 @@ func (s *TestServer) MoveResourceState(_ context.Context, req *tfprotov5.MoveRes return nil, nil } +func (s *TestServer) OpenEphemeralResource(_ context.Context, req *tfprotov5.OpenEphemeralResourceRequest) (*tfprotov5.OpenEphemeralResourceResponse, error) { + if s.OpenEphemeralResourceCalled == nil { + s.OpenEphemeralResourceCalled = make(map[string]bool) + } + + s.OpenEphemeralResourceCalled[req.TypeName] = true + return nil, nil +} + func (s *TestServer) PlanResourceChange(_ context.Context, req *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) { if s.PlanResourceChangeCalled == nil { s.PlanResourceChangeCalled = make(map[string]bool) @@ -164,6 +190,15 @@ func (s *TestServer) ReadResource(_ context.Context, req *tfprotov5.ReadResource return nil, nil } +func (s *TestServer) RenewEphemeralResource(_ context.Context, req *tfprotov5.RenewEphemeralResourceRequest) (*tfprotov5.RenewEphemeralResourceResponse, error) { + if s.RenewEphemeralResourceCalled == nil { + s.RenewEphemeralResourceCalled = make(map[string]bool) + } + + s.RenewEphemeralResourceCalled[req.TypeName] = true + return nil, nil +} + func (s *TestServer) StopProvider(_ context.Context, _ *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) { s.StopProviderCalled = true @@ -183,6 +218,15 @@ func (s *TestServer) UpgradeResourceState(_ context.Context, req *tfprotov5.Upgr return nil, nil } +func (s *TestServer) ValidateEphemeralResourceConfig(_ context.Context, req *tfprotov5.ValidateEphemeralResourceConfigRequest) (*tfprotov5.ValidateEphemeralResourceConfigResponse, error) { + if s.ValidateEphemeralResourceConfigCalled == nil { + s.ValidateEphemeralResourceConfigCalled = make(map[string]bool) + } + + s.ValidateEphemeralResourceConfigCalled[req.TypeName] = true + return nil, nil +} + func (s *TestServer) ValidateDataSourceConfig(_ context.Context, req *tfprotov5.ValidateDataSourceConfigRequest) (*tfprotov5.ValidateDataSourceConfigResponse, error) { if s.ValidateDataSourceConfigCalled == nil { s.ValidateDataSourceConfigCalled = make(map[string]bool) diff --git a/internal/tf6testserver/tf6testserver.go b/internal/tf6testserver/tf6testserver.go index 8687efd..3c605f0 100644 --- a/internal/tf6testserver/tf6testserver.go +++ b/internal/tf6testserver/tf6testserver.go @@ -18,6 +18,8 @@ type TestServer struct { CallFunctionCalled map[string]bool + CloseEphemeralResourceCalled map[string]bool + ConfigureProviderCalled bool ConfigureProviderResponse *tfprotov6.ConfigureProviderResponse @@ -34,12 +36,16 @@ type TestServer struct { MoveResourceStateCalled map[string]bool + OpenEphemeralResourceCalled map[string]bool + PlanResourceChangeCalled map[string]bool ReadDataSourceCalled map[string]bool ReadResourceCalled map[string]bool + RenewEphemeralResourceCalled map[string]bool + StopProviderCalled bool StopProviderResponse *tfprotov6.StopProviderResponse @@ -47,6 +53,8 @@ type TestServer struct { ValidateDataResourceConfigCalled map[string]bool + ValidateEphemeralResourceConfigCalled map[string]bool + ValidateProviderConfigCalled bool ValidateProviderConfigResponse *tfprotov6.ValidateProviderConfigResponse @@ -75,6 +83,15 @@ func (s *TestServer) CallFunction(_ context.Context, req *tfprotov6.CallFunction return nil, nil } +func (s *TestServer) CloseEphemeralResource(ctx context.Context, req *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) { + if s.CloseEphemeralResourceCalled == nil { + s.CloseEphemeralResourceCalled = make(map[string]bool) + } + + s.CloseEphemeralResourceCalled[req.TypeName] = true + return nil, nil +} + func (s *TestServer) ConfigureProvider(_ context.Context, _ *tfprotov6.ConfigureProviderRequest) (*tfprotov6.ConfigureProviderResponse, error) { s.ConfigureProviderCalled = true @@ -137,6 +154,15 @@ func (s *TestServer) MoveResourceState(_ context.Context, req *tfprotov6.MoveRes return nil, nil } +func (s *TestServer) OpenEphemeralResource(_ context.Context, req *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) { + if s.OpenEphemeralResourceCalled == nil { + s.OpenEphemeralResourceCalled = make(map[string]bool) + } + + s.OpenEphemeralResourceCalled[req.TypeName] = true + return nil, nil +} + func (s *TestServer) PlanResourceChange(_ context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { if s.PlanResourceChangeCalled == nil { s.PlanResourceChangeCalled = make(map[string]bool) @@ -164,6 +190,15 @@ func (s *TestServer) ReadResource(_ context.Context, req *tfprotov6.ReadResource return nil, nil } +func (s *TestServer) RenewEphemeralResource(_ context.Context, req *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) { + if s.RenewEphemeralResourceCalled == nil { + s.RenewEphemeralResourceCalled = make(map[string]bool) + } + + s.RenewEphemeralResourceCalled[req.TypeName] = true + return nil, nil +} + func (s *TestServer) StopProvider(_ context.Context, _ *tfprotov6.StopProviderRequest) (*tfprotov6.StopProviderResponse, error) { s.StopProviderCalled = true @@ -183,6 +218,15 @@ func (s *TestServer) UpgradeResourceState(_ context.Context, req *tfprotov6.Upgr return nil, nil } +func (s *TestServer) ValidateEphemeralResourceConfig(_ context.Context, req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { + if s.ValidateEphemeralResourceConfigCalled == nil { + s.ValidateEphemeralResourceConfigCalled = make(map[string]bool) + } + + s.ValidateEphemeralResourceConfigCalled[req.TypeName] = true + return nil, nil +} + func (s *TestServer) ValidateDataResourceConfig(_ context.Context, req *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { if s.ValidateDataResourceConfigCalled == nil { s.ValidateDataResourceConfigCalled = make(map[string]bool) diff --git a/internal/tfprotov5tov6/tfprotov5tov6.go b/internal/tfprotov5tov6/tfprotov5tov6.go index 9490c82..faf8fbd 100644 --- a/internal/tfprotov5tov6/tfprotov5tov6.go +++ b/internal/tfprotov5tov6/tfprotov5tov6.go @@ -64,6 +64,27 @@ func CallFunctionResponse(in *tfprotov5.CallFunctionResponse) *tfprotov6.CallFun } } +func CloseEphemeralResourceRequest(in *tfprotov5.CloseEphemeralResourceRequest) *tfprotov6.CloseEphemeralResourceRequest { + if in == nil { + return nil + } + + return &tfprotov6.CloseEphemeralResourceRequest{ + TypeName: in.TypeName, + Private: in.Private, + } +} + +func CloseEphemeralResourceResponse(in *tfprotov5.CloseEphemeralResourceResponse) *tfprotov6.CloseEphemeralResourceResponse { + if in == nil { + return nil + } + + return &tfprotov6.CloseEphemeralResourceResponse{ + Diagnostics: Diagnostics(in.Diagnostics), + } +} + func ConfigureProviderRequest(in *tfprotov5.ConfigureProviderRequest) *tfprotov6.ConfigureProviderRequest { if in == nil { return nil @@ -151,6 +172,12 @@ func DynamicValue(in *tfprotov5.DynamicValue) *tfprotov6.DynamicValue { } } +func EphemeralResourceMetadata(in tfprotov5.EphemeralResourceMetadata) tfprotov6.EphemeralResourceMetadata { + return tfprotov6.EphemeralResourceMetadata{ + TypeName: in.TypeName, + } +} + func Function(in *tfprotov5.Function) *tfprotov6.Function { if in == nil { return nil @@ -258,6 +285,7 @@ func GetMetadataResponse(in *tfprotov5.GetMetadataResponse) *tfprotov6.GetMetada resp := &tfprotov6.GetMetadataResponse{ DataSources: make([]tfprotov6.DataSourceMetadata, 0, len(in.DataSources)), Diagnostics: Diagnostics(in.Diagnostics), + EphemeralResources: make([]tfprotov6.EphemeralResourceMetadata, 0, len(in.Resources)), Functions: make([]tfprotov6.FunctionMetadata, 0, len(in.Functions)), Resources: make([]tfprotov6.ResourceMetadata, 0, len(in.Resources)), ServerCapabilities: ServerCapabilities(in.ServerCapabilities), @@ -267,6 +295,10 @@ func GetMetadataResponse(in *tfprotov5.GetMetadataResponse) *tfprotov6.GetMetada resp.DataSources = append(resp.DataSources, DataSourceMetadata(datasource)) } + for _, ephemeralResource := range in.EphemeralResources { + resp.EphemeralResources = append(resp.EphemeralResources, EphemeralResourceMetadata(ephemeralResource)) + } + for _, function := range in.Functions { resp.Functions = append(resp.Functions, FunctionMetadata(function)) } @@ -297,6 +329,12 @@ func GetProviderSchemaResponse(in *tfprotov5.GetProviderSchemaResponse) *tfproto dataSourceSchemas[k] = Schema(v) } + ephemeralResourceSchemas := make(map[string]*tfprotov6.Schema, len(in.EphemeralResourceSchemas)) + + for k, v := range in.EphemeralResourceSchemas { + ephemeralResourceSchemas[k] = Schema(v) + } + functions := make(map[string]*tfprotov6.Function, len(in.Functions)) for name, function := range in.Functions { @@ -310,13 +348,14 @@ func GetProviderSchemaResponse(in *tfprotov5.GetProviderSchemaResponse) *tfproto } return &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: dataSourceSchemas, - Diagnostics: Diagnostics(in.Diagnostics), - Functions: functions, - Provider: Schema(in.Provider), - ProviderMeta: Schema(in.ProviderMeta), - ResourceSchemas: resourceSchemas, - ServerCapabilities: ServerCapabilities(in.ServerCapabilities), + DataSourceSchemas: dataSourceSchemas, + Diagnostics: Diagnostics(in.Diagnostics), + EphemeralResourceSchemas: ephemeralResourceSchemas, + Functions: functions, + Provider: Schema(in.Provider), + ProviderMeta: Schema(in.ProviderMeta), + ResourceSchemas: resourceSchemas, + ServerCapabilities: ServerCapabilities(in.ServerCapabilities), } } @@ -406,6 +445,44 @@ func MoveResourceStateResponse(in *tfprotov5.MoveResourceStateResponse) *tfproto } } +func OpenEphemeralResourceRequest(in *tfprotov5.OpenEphemeralResourceRequest) *tfprotov6.OpenEphemeralResourceRequest { + if in == nil { + return nil + } + + return &tfprotov6.OpenEphemeralResourceRequest{ + TypeName: in.TypeName, + Config: DynamicValue(in.Config), + ClientCapabilities: OpenEphemeralResourceClientCapabilities(in.ClientCapabilities), + } +} + +func OpenEphemeralResourceClientCapabilities(in *tfprotov5.OpenEphemeralResourceClientCapabilities) *tfprotov6.OpenEphemeralResourceClientCapabilities { + if in == nil { + return nil + } + + resp := &tfprotov6.OpenEphemeralResourceClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} + +func OpenEphemeralResourceResponse(in *tfprotov5.OpenEphemeralResourceResponse) *tfprotov6.OpenEphemeralResourceResponse { + if in == nil { + return nil + } + + return &tfprotov6.OpenEphemeralResourceResponse{ + Result: DynamicValue(in.Result), + Diagnostics: Diagnostics(in.Diagnostics), + Private: in.Private, + RenewAt: in.RenewAt, + Deferred: Deferred(in.Deferred), + } +} + func PlanResourceChangeRequest(in *tfprotov5.PlanResourceChangeRequest) *tfprotov6.PlanResourceChangeRequest { if in == nil { return nil @@ -535,6 +612,29 @@ func ReadResourceResponse(in *tfprotov5.ReadResourceResponse) *tfprotov6.ReadRes } } +func RenewEphemeralResourceRequest(in *tfprotov5.RenewEphemeralResourceRequest) *tfprotov6.RenewEphemeralResourceRequest { + if in == nil { + return nil + } + + return &tfprotov6.RenewEphemeralResourceRequest{ + TypeName: in.TypeName, + Private: in.Private, + } +} + +func RenewEphemeralResourceResponse(in *tfprotov5.RenewEphemeralResourceResponse) *tfprotov6.RenewEphemeralResourceResponse { + if in == nil { + return nil + } + + return &tfprotov6.RenewEphemeralResourceResponse{ + Diagnostics: Diagnostics(in.Diagnostics), + Private: in.Private, + RenewAt: in.RenewAt, + } +} + func ResourceMetadata(in tfprotov5.ResourceMetadata) tfprotov6.ResourceMetadata { return tfprotov6.ResourceMetadata{ TypeName: in.TypeName, @@ -676,6 +776,27 @@ func UpgradeResourceStateResponse(in *tfprotov5.UpgradeResourceStateResponse) *t } } +func ValidateEphemeralResourceConfigRequest(in *tfprotov5.ValidateEphemeralResourceConfigRequest) *tfprotov6.ValidateEphemeralResourceConfigRequest { + if in == nil { + return nil + } + + return &tfprotov6.ValidateEphemeralResourceConfigRequest{ + Config: DynamicValue(in.Config), + TypeName: in.TypeName, + } +} + +func ValidateEphemeralResourceConfigResponse(in *tfprotov5.ValidateEphemeralResourceConfigResponse) *tfprotov6.ValidateEphemeralResourceConfigResponse { + if in == nil { + return nil + } + + return &tfprotov6.ValidateEphemeralResourceConfigResponse{ + Diagnostics: Diagnostics(in.Diagnostics), + } +} + func ValidateDataResourceConfigRequest(in *tfprotov5.ValidateDataSourceConfigRequest) *tfprotov6.ValidateDataResourceConfigRequest { if in == nil { return nil diff --git a/internal/tfprotov5tov6/tfprotov5tov6_test.go b/internal/tfprotov5tov6/tfprotov5tov6_test.go index 1e15d90..48b18a3 100644 --- a/internal/tfprotov5tov6/tfprotov5tov6_test.go +++ b/internal/tfprotov5tov6/tfprotov5tov6_test.go @@ -5,6 +5,7 @@ package tfprotov5tov6_test import ( "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-go/tfprotov5" @@ -43,6 +44,14 @@ var ( testTfprotov5DynamicValue tfprotov5.DynamicValue testTfprotov6DynamicValue tfprotov6.DynamicValue + testTfprotov5EphemeralResourceMetadata tfprotov5.EphemeralResourceMetadata = tfprotov5.EphemeralResourceMetadata{ + TypeName: "test_ephemeral_resource", + } + + testTfprotov6EphemeralResourceMetadata tfprotov6.EphemeralResourceMetadata = tfprotov6.EphemeralResourceMetadata{ + TypeName: "test_ephemeral_resource", + } + testTfprotov5Function *tfprotov5.Function = &tfprotov5.Function{ Parameters: []*tfprotov5.FunctionParameter{}, Return: &tfprotov5.FunctionReturn{ @@ -107,6 +116,8 @@ var ( }, Version: 1, } + + testTime time.Time = time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC) ) func init() { @@ -282,6 +293,80 @@ func TestCallFunctionResponse(t *testing.T) { } } +func TestCloseEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov5.CloseEphemeralResourceRequest + expected *tfprotov6.CloseEphemeralResourceRequest + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov5.CloseEphemeralResourceRequest{ + Private: testBytes, + TypeName: "test_ephemeral_resource", + }, + expected: &tfprotov6.CloseEphemeralResourceRequest{ + Private: testBytes, + TypeName: "test_ephemeral_resource", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov5tov6.CloseEphemeralResourceRequest(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestCloseEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov5.CloseEphemeralResourceResponse + expected *tfprotov6.CloseEphemeralResourceResponse + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov5.CloseEphemeralResourceResponse{ + Diagnostics: testTfprotov5Diagnostics, + }, + expected: &tfprotov6.CloseEphemeralResourceResponse{ + Diagnostics: testTfprotov6Diagnostics, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov5tov6.CloseEphemeralResourceResponse(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestConfigureProviderRequest(t *testing.T) { t.Parallel() @@ -767,6 +852,9 @@ func TestGetMetadataResponse(t *testing.T) { testTfprotov5DataSourceMetadata, }, Diagnostics: testTfprotov5Diagnostics, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + testTfprotov5EphemeralResourceMetadata, + }, Functions: []tfprotov5.FunctionMetadata{ testTfprotov5FunctionMetadata, }, @@ -779,6 +867,9 @@ func TestGetMetadataResponse(t *testing.T) { testTfprotov6DataSourceMetadata, }, Diagnostics: testTfprotov6Diagnostics, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + testTfprotov6EphemeralResourceMetadata, + }, Functions: []tfprotov6.FunctionMetadata{ testTfprotov6FunctionMetadata, }, @@ -853,6 +944,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { "test_data_source": testTfprotov5Schema, }, Diagnostics: testTfprotov5Diagnostics, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": testTfprotov5Schema, + }, Functions: map[string]*tfprotov5.Function{ "test_function": testTfprotov5Function, }, @@ -867,6 +961,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { "test_data_source": testTfprotov6Schema, }, Diagnostics: testTfprotov6Diagnostics, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": testTfprotov6Schema, + }, Functions: map[string]*tfprotov6.Function{ "test_function": testTfprotov6Function, }, @@ -1191,6 +1288,118 @@ func TestMoveResourceStateResponse(t *testing.T) { } } +func TestOpenEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov5.OpenEphemeralResourceRequest + expected *tfprotov6.OpenEphemeralResourceRequest + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov5.OpenEphemeralResourceRequest{ + Config: &testTfprotov5DynamicValue, + TypeName: "test_ephemeral_resource", + }, + expected: &tfprotov6.OpenEphemeralResourceRequest{ + Config: &testTfprotov6DynamicValue, + TypeName: "test_ephemeral_resource", + }, + }, + "client-capabilities-deferral-allowed": { + in: &tfprotov5.OpenEphemeralResourceRequest{ + Config: &testTfprotov5DynamicValue, + TypeName: "test_ephemeral_resource", + ClientCapabilities: &tfprotov5.OpenEphemeralResourceClientCapabilities{ + DeferralAllowed: true, + }, + }, + expected: &tfprotov6.OpenEphemeralResourceRequest{ + Config: &testTfprotov6DynamicValue, + TypeName: "test_ephemeral_resource", + ClientCapabilities: &tfprotov6.OpenEphemeralResourceClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov5tov6.OpenEphemeralResourceRequest(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestOpenEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov5.OpenEphemeralResourceResponse + expected *tfprotov6.OpenEphemeralResourceResponse + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: testTfprotov5Diagnostics, + Private: testBytes, + RenewAt: testTime, + Result: &testTfprotov5DynamicValue, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: testTfprotov6Diagnostics, + Private: testBytes, + RenewAt: testTime, + Result: &testTfprotov6DynamicValue, + }, + }, + "deferred-reason": { + in: &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: testTfprotov5Diagnostics, + Result: &testTfprotov5DynamicValue, + Deferred: &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonResourceConfigUnknown, + }, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: testTfprotov6Diagnostics, + Result: &testTfprotov6DynamicValue, + Deferred: &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonResourceConfigUnknown, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov5tov6.OpenEphemeralResourceResponse(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestPlanResourceChangeRequest(t *testing.T) { t.Parallel() @@ -1605,6 +1814,84 @@ func TestReadResourceResponse(t *testing.T) { } } +func TestRenewEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov5.RenewEphemeralResourceRequest + expected *tfprotov6.RenewEphemeralResourceRequest + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov5.RenewEphemeralResourceRequest{ + Private: testBytes, + TypeName: "test_ephemeral_resource", + }, + expected: &tfprotov6.RenewEphemeralResourceRequest{ + Private: testBytes, + TypeName: "test_ephemeral_resource", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov5tov6.RenewEphemeralResourceRequest(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestRenewEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov5.RenewEphemeralResourceResponse + expected *tfprotov6.RenewEphemeralResourceResponse + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov5.RenewEphemeralResourceResponse{ + Diagnostics: testTfprotov5Diagnostics, + Private: testBytes, + RenewAt: testTime, + }, + expected: &tfprotov6.RenewEphemeralResourceResponse{ + Diagnostics: testTfprotov6Diagnostics, + Private: testBytes, + RenewAt: testTime, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov5tov6.RenewEphemeralResourceResponse(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSchema(t *testing.T) { t.Parallel() @@ -2083,6 +2370,80 @@ func TestValidateDataResourceConfigResponse(t *testing.T) { } } +func TestValidateEphemeralResourceConfigRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov5.ValidateEphemeralResourceConfigRequest + expected *tfprotov6.ValidateEphemeralResourceConfigRequest + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + Config: &testTfprotov5DynamicValue, + TypeName: "test_ephemeral_resource", + }, + expected: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + Config: &testTfprotov6DynamicValue, + TypeName: "test_ephemeral_resource", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov5tov6.ValidateEphemeralResourceConfigRequest(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestValidateEphemeralResourceConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov5.ValidateEphemeralResourceConfigResponse + expected *tfprotov6.ValidateEphemeralResourceConfigResponse + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov5.ValidateEphemeralResourceConfigResponse{ + Diagnostics: testTfprotov5Diagnostics, + }, + expected: &tfprotov6.ValidateEphemeralResourceConfigResponse{ + Diagnostics: testTfprotov6Diagnostics, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov5tov6.ValidateEphemeralResourceConfigResponse(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestValidateProviderConfigRequest(t *testing.T) { t.Parallel() diff --git a/internal/tfprotov6tov5/tfprotov6tov5.go b/internal/tfprotov6tov5/tfprotov6tov5.go index 1d04936..1db72cd 100644 --- a/internal/tfprotov6tov5/tfprotov6tov5.go +++ b/internal/tfprotov6tov5/tfprotov6tov5.go @@ -69,6 +69,27 @@ func CallFunctionResponse(in *tfprotov6.CallFunctionResponse) *tfprotov5.CallFun } } +func CloseEphemeralResourceRequest(in *tfprotov6.CloseEphemeralResourceRequest) *tfprotov5.CloseEphemeralResourceRequest { + if in == nil { + return nil + } + + return &tfprotov5.CloseEphemeralResourceRequest{ + TypeName: in.TypeName, + Private: in.Private, + } +} + +func CloseEphemeralResourceResponse(in *tfprotov6.CloseEphemeralResourceResponse) *tfprotov5.CloseEphemeralResourceResponse { + if in == nil { + return nil + } + + return &tfprotov5.CloseEphemeralResourceResponse{ + Diagnostics: Diagnostics(in.Diagnostics), + } +} + func ConfigureProviderRequest(in *tfprotov6.ConfigureProviderRequest) *tfprotov5.ConfigureProviderRequest { if in == nil { return nil @@ -156,6 +177,12 @@ func DynamicValue(in *tfprotov6.DynamicValue) *tfprotov5.DynamicValue { } } +func EphemeralResourceMetadata(in tfprotov6.EphemeralResourceMetadata) tfprotov5.EphemeralResourceMetadata { + return tfprotov5.EphemeralResourceMetadata{ + TypeName: in.TypeName, + } +} + func Function(in *tfprotov6.Function) *tfprotov5.Function { if in == nil { return nil @@ -263,6 +290,7 @@ func GetMetadataResponse(in *tfprotov6.GetMetadataResponse) *tfprotov5.GetMetada resp := &tfprotov5.GetMetadataResponse{ DataSources: make([]tfprotov5.DataSourceMetadata, 0, len(in.DataSources)), Diagnostics: Diagnostics(in.Diagnostics), + EphemeralResources: make([]tfprotov5.EphemeralResourceMetadata, 0, len(in.Resources)), Functions: make([]tfprotov5.FunctionMetadata, 0, len(in.Functions)), Resources: make([]tfprotov5.ResourceMetadata, 0, len(in.Resources)), ServerCapabilities: ServerCapabilities(in.ServerCapabilities), @@ -272,6 +300,10 @@ func GetMetadataResponse(in *tfprotov6.GetMetadataResponse) *tfprotov5.GetMetada resp.DataSources = append(resp.DataSources, DataSourceMetadata(datasource)) } + for _, ephemeralResource := range in.EphemeralResources { + resp.EphemeralResources = append(resp.EphemeralResources, EphemeralResourceMetadata(ephemeralResource)) + } + for _, function := range in.Functions { resp.Functions = append(resp.Functions, FunctionMetadata(function)) } @@ -308,6 +340,18 @@ func GetProviderSchemaResponse(in *tfprotov6.GetProviderSchemaResponse) (*tfprot dataSourceSchemas[k] = v5Schema } + ephemeralResourceSchemas := make(map[string]*tfprotov5.Schema, len(in.EphemeralResourceSchemas)) + + for k, v := range in.EphemeralResourceSchemas { + v5Schema, err := Schema(v) + + if err != nil { + return nil, fmt.Errorf("unable to convert ephemeral resource %q schema: %w", k, err) + } + + ephemeralResourceSchemas[k] = v5Schema + } + functions := make(map[string]*tfprotov5.Function, len(in.Functions)) for name, function := range in.Functions { @@ -339,12 +383,13 @@ func GetProviderSchemaResponse(in *tfprotov6.GetProviderSchemaResponse) (*tfprot } return &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: dataSourceSchemas, - Diagnostics: Diagnostics(in.Diagnostics), - Functions: functions, - Provider: provider, - ProviderMeta: providerMeta, - ResourceSchemas: resourceSchemas, + DataSourceSchemas: dataSourceSchemas, + Diagnostics: Diagnostics(in.Diagnostics), + EphemeralResourceSchemas: ephemeralResourceSchemas, + Functions: functions, + Provider: provider, + ProviderMeta: providerMeta, + ResourceSchemas: resourceSchemas, }, nil } @@ -434,6 +479,44 @@ func MoveResourceStateResponse(in *tfprotov6.MoveResourceStateResponse) *tfproto } } +func OpenEphemeralResourceRequest(in *tfprotov6.OpenEphemeralResourceRequest) *tfprotov5.OpenEphemeralResourceRequest { + if in == nil { + return nil + } + + return &tfprotov5.OpenEphemeralResourceRequest{ + TypeName: in.TypeName, + Config: DynamicValue(in.Config), + ClientCapabilities: OpenEphemeralResourceClientCapabilities(in.ClientCapabilities), + } +} + +func OpenEphemeralResourceClientCapabilities(in *tfprotov6.OpenEphemeralResourceClientCapabilities) *tfprotov5.OpenEphemeralResourceClientCapabilities { + if in == nil { + return nil + } + + resp := &tfprotov5.OpenEphemeralResourceClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } + + return resp +} + +func OpenEphemeralResourceResponse(in *tfprotov6.OpenEphemeralResourceResponse) *tfprotov5.OpenEphemeralResourceResponse { + if in == nil { + return nil + } + + return &tfprotov5.OpenEphemeralResourceResponse{ + Result: DynamicValue(in.Result), + Diagnostics: Diagnostics(in.Diagnostics), + Private: in.Private, + RenewAt: in.RenewAt, + Deferred: Deferred(in.Deferred), + } +} + func PlanResourceChangeRequest(in *tfprotov6.PlanResourceChangeRequest) *tfprotov5.PlanResourceChangeRequest { if in == nil { return nil @@ -583,6 +666,29 @@ func ReadResourceResponse(in *tfprotov6.ReadResourceResponse) *tfprotov5.ReadRes } } +func RenewEphemeralResourceRequest(in *tfprotov6.RenewEphemeralResourceRequest) *tfprotov5.RenewEphemeralResourceRequest { + if in == nil { + return nil + } + + return &tfprotov5.RenewEphemeralResourceRequest{ + TypeName: in.TypeName, + Private: in.Private, + } +} + +func RenewEphemeralResourceResponse(in *tfprotov6.RenewEphemeralResourceResponse) *tfprotov5.RenewEphemeralResourceResponse { + if in == nil { + return nil + } + + return &tfprotov5.RenewEphemeralResourceResponse{ + Diagnostics: Diagnostics(in.Diagnostics), + Private: in.Private, + RenewAt: in.RenewAt, + } +} + func ResourceMetadata(in tfprotov6.ResourceMetadata) tfprotov5.ResourceMetadata { return tfprotov5.ResourceMetadata{ TypeName: in.TypeName, @@ -773,6 +879,27 @@ func ValidateDataSourceConfigResponse(in *tfprotov6.ValidateDataResourceConfigRe } } +func ValidateEphemeralResourceConfigRequest(in *tfprotov6.ValidateEphemeralResourceConfigRequest) *tfprotov5.ValidateEphemeralResourceConfigRequest { + if in == nil { + return nil + } + + return &tfprotov5.ValidateEphemeralResourceConfigRequest{ + Config: DynamicValue(in.Config), + TypeName: in.TypeName, + } +} + +func ValidateEphemeralResourceConfigResponse(in *tfprotov6.ValidateEphemeralResourceConfigResponse) *tfprotov5.ValidateEphemeralResourceConfigResponse { + if in == nil { + return nil + } + + return &tfprotov5.ValidateEphemeralResourceConfigResponse{ + Diagnostics: Diagnostics(in.Diagnostics), + } +} + func ValidateResourceTypeConfigRequest(in *tfprotov6.ValidateResourceConfigRequest) *tfprotov5.ValidateResourceTypeConfigRequest { if in == nil { return nil diff --git a/internal/tfprotov6tov5/tfprotov6tov5_test.go b/internal/tfprotov6tov5/tfprotov6tov5_test.go index 9252d42..e170f2c 100644 --- a/internal/tfprotov6tov5/tfprotov6tov5_test.go +++ b/internal/tfprotov6tov5/tfprotov6tov5_test.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-go/tfprotov5" @@ -45,6 +46,14 @@ var ( testTfprotov5DynamicValue tfprotov5.DynamicValue testTfprotov6DynamicValue tfprotov6.DynamicValue + testTfprotov5EphemeralResourceMetadata tfprotov5.EphemeralResourceMetadata = tfprotov5.EphemeralResourceMetadata{ + TypeName: "test_ephemeral_resource", + } + + testTfprotov6EphemeralResourceMetadata tfprotov6.EphemeralResourceMetadata = tfprotov6.EphemeralResourceMetadata{ + TypeName: "test_ephemeral_resource", + } + testTfprotov5Function *tfprotov5.Function = &tfprotov5.Function{ Parameters: []*tfprotov5.FunctionParameter{}, Return: &tfprotov5.FunctionReturn{ @@ -109,6 +118,8 @@ var ( }, Version: 1, } + + testTime time.Time = time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC) ) func init() { @@ -284,6 +295,80 @@ func TestCallFunctionResponse(t *testing.T) { } } +func TestCloseEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov6.CloseEphemeralResourceRequest + expected *tfprotov5.CloseEphemeralResourceRequest + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov6.CloseEphemeralResourceRequest{ + Private: testBytes, + TypeName: "test_ephemeral_resource", + }, + expected: &tfprotov5.CloseEphemeralResourceRequest{ + Private: testBytes, + TypeName: "test_ephemeral_resource", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov6tov5.CloseEphemeralResourceRequest(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestCloseEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov6.CloseEphemeralResourceResponse + expected *tfprotov5.CloseEphemeralResourceResponse + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov6.CloseEphemeralResourceResponse{ + Diagnostics: testTfprotov6Diagnostics, + }, + expected: &tfprotov5.CloseEphemeralResourceResponse{ + Diagnostics: testTfprotov5Diagnostics, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov6tov5.CloseEphemeralResourceResponse(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestConfigureProviderRequest(t *testing.T) { t.Parallel() @@ -769,6 +854,9 @@ func TestGetMetadataResponse(t *testing.T) { testTfprotov6DataSourceMetadata, }, Diagnostics: testTfprotov6Diagnostics, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + testTfprotov6EphemeralResourceMetadata, + }, Functions: []tfprotov6.FunctionMetadata{ testTfprotov6FunctionMetadata, }, @@ -781,6 +869,9 @@ func TestGetMetadataResponse(t *testing.T) { testTfprotov5DataSourceMetadata, }, Diagnostics: testTfprotov5Diagnostics, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + testTfprotov5EphemeralResourceMetadata, + }, Functions: []tfprotov5.FunctionMetadata{ testTfprotov5FunctionMetadata, }, @@ -856,6 +947,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { "test_data_source": testTfprotov6Schema, }, Diagnostics: testTfprotov6Diagnostics, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": testTfprotov6Schema, + }, Functions: map[string]*tfprotov6.Function{ "test_function": testTfprotov6Function, }, @@ -870,6 +964,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { "test_data_source": testTfprotov5Schema, }, Diagnostics: testTfprotov5Diagnostics, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": testTfprotov5Schema, + }, Functions: map[string]*tfprotov5.Function{ "test_function": testTfprotov5Function, }, @@ -906,6 +1003,32 @@ func TestGetProviderSchemaResponse(t *testing.T) { expected: nil, expectedError: fmt.Errorf("unable to convert data source \"test_data_source\" schema: unable to convert attribute \"test_attribute\" schema: %w", tfprotov6tov5.ErrSchemaAttributeNestedTypeNotImplemented), }, + "ephemeral-resource-nested-attribute-error": { + in: &tfprotov6.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_nested_attribute", + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + }, + expected: nil, + expectedError: fmt.Errorf("unable to convert ephemeral resource \"test_ephemeral_resource\" schema: unable to convert attribute \"test_attribute\" schema: %w", tfprotov6tov5.ErrSchemaAttributeNestedTypeNotImplemented), + }, "provider-nested-attribute-error": { in: &tfprotov6.GetProviderSchemaResponse{ Provider: &tfprotov6.Schema{ @@ -1306,6 +1429,118 @@ func TestMoveResourceStateResponse(t *testing.T) { } } +func TestOpenEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov6.OpenEphemeralResourceRequest + expected *tfprotov5.OpenEphemeralResourceRequest + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov6.OpenEphemeralResourceRequest{ + Config: &testTfprotov6DynamicValue, + TypeName: "test_ephemeral_resource", + }, + expected: &tfprotov5.OpenEphemeralResourceRequest{ + Config: &testTfprotov5DynamicValue, + TypeName: "test_ephemeral_resource", + }, + }, + "client-capabilities-deferral-allowed": { + in: &tfprotov6.OpenEphemeralResourceRequest{ + Config: &testTfprotov6DynamicValue, + TypeName: "test_ephemeral_resource", + ClientCapabilities: &tfprotov6.OpenEphemeralResourceClientCapabilities{ + DeferralAllowed: true, + }, + }, + expected: &tfprotov5.OpenEphemeralResourceRequest{ + Config: &testTfprotov5DynamicValue, + TypeName: "test_ephemeral_resource", + ClientCapabilities: &tfprotov5.OpenEphemeralResourceClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov6tov5.OpenEphemeralResourceRequest(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestOpenEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov6.OpenEphemeralResourceResponse + expected *tfprotov5.OpenEphemeralResourceResponse + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: testTfprotov6Diagnostics, + Private: testBytes, + RenewAt: testTime, + Result: &testTfprotov6DynamicValue, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: testTfprotov5Diagnostics, + Private: testBytes, + RenewAt: testTime, + Result: &testTfprotov5DynamicValue, + }, + }, + "deferred-reason": { + in: &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: testTfprotov6Diagnostics, + Result: &testTfprotov6DynamicValue, + Deferred: &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonResourceConfigUnknown, + }, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: testTfprotov5Diagnostics, + Result: &testTfprotov5DynamicValue, + Deferred: &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonResourceConfigUnknown, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov6tov5.OpenEphemeralResourceResponse(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestPlanResourceChangeRequest(t *testing.T) { t.Parallel() @@ -1794,6 +2029,84 @@ func TestReadResourceResponse(t *testing.T) { } } +func TestRenewEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov6.RenewEphemeralResourceRequest + expected *tfprotov5.RenewEphemeralResourceRequest + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov6.RenewEphemeralResourceRequest{ + Private: testBytes, + TypeName: "test_ephemeral_resource", + }, + expected: &tfprotov5.RenewEphemeralResourceRequest{ + Private: testBytes, + TypeName: "test_ephemeral_resource", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov6tov5.RenewEphemeralResourceRequest(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestRenewEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov6.RenewEphemeralResourceResponse + expected *tfprotov5.RenewEphemeralResourceResponse + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov6.RenewEphemeralResourceResponse{ + Diagnostics: testTfprotov6Diagnostics, + Private: testBytes, + RenewAt: testTime, + }, + expected: &tfprotov5.RenewEphemeralResourceResponse{ + Diagnostics: testTfprotov5Diagnostics, + Private: testBytes, + RenewAt: testTime, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov6tov5.RenewEphemeralResourceResponse(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSchema(t *testing.T) { t.Parallel() @@ -2406,6 +2719,80 @@ func TestValidateDataSourceConfigResponse(t *testing.T) { } } +func TestValidateEphemeralResourceConfigRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov6.ValidateEphemeralResourceConfigRequest + expected *tfprotov5.ValidateEphemeralResourceConfigRequest + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + Config: &testTfprotov6DynamicValue, + TypeName: "test_ephemeral_resource", + }, + expected: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + Config: &testTfprotov5DynamicValue, + TypeName: "test_ephemeral_resource", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov6tov5.ValidateEphemeralResourceConfigRequest(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestValidateEphemeralResourceConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov6.ValidateEphemeralResourceConfigResponse + expected *tfprotov5.ValidateEphemeralResourceConfigResponse + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov6.ValidateEphemeralResourceConfigResponse{ + Diagnostics: testTfprotov6Diagnostics, + }, + expected: &tfprotov5.ValidateEphemeralResourceConfigResponse{ + Diagnostics: testTfprotov5Diagnostics, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov6tov5.ValidateEphemeralResourceConfigResponse(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestValidateResourceTypeConfigRequest(t *testing.T) { t.Parallel() diff --git a/tf5muxserver/diagnostics.go b/tf5muxserver/diagnostics.go index 4348d23..e0761ff 100644 --- a/tf5muxserver/diagnostics.go +++ b/tf5muxserver/diagnostics.go @@ -26,6 +26,27 @@ func dataSourceMissingError(typeName string) *tfprotov5.Diagnostic { } } +func ephemeralResourceDuplicateError(typeName string) *tfprotov5.Diagnostic { + return &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same ephemeral resource type across underlying providers. " + + "Ephemeral resource types must be implemented by only one underlying provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate ephemeral resource type: " + typeName, + } +} + +func ephemeralResourceMissingError(typeName string) *tfprotov5.Diagnostic { + return &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Ephemeral Resource Not Implemented", + Detail: "The combined provider does not implement the requested ephemeral resource type. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Missing ephemeral resource type: " + typeName, + } +} + func diagnosticsHasError(diagnostics []*tfprotov5.Diagnostic) bool { for _, diagnostic := range diagnostics { if diagnostic == nil { diff --git a/tf5muxserver/mux_server.go b/tf5muxserver/mux_server.go index 91137db..ba23971 100644 --- a/tf5muxserver/mux_server.go +++ b/tf5muxserver/mux_server.go @@ -8,9 +8,10 @@ import ( "sync" "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-mux/internal/logging" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" ) var _ tfprotov5.ProviderServer = &muxServer{} @@ -22,6 +23,9 @@ type muxServer struct { // Routing for data source types dataSources map[string]tfprotov5.ProviderServer + // Routing for ephemeral resource types + ephemeralResources map[string]tfprotov5.ProviderServer + // Routing for functions functions map[string]tfprotov5.ProviderServer @@ -90,6 +94,41 @@ func (s *muxServer) getDataSourceServer(ctx context.Context, typeName string) (t return server, s.serverDiscoveryDiagnostics, nil } +func (s *muxServer) getEphemeralResourceServer(ctx context.Context, typeName string) (tfprotov5.ProviderServer, []*tfprotov5.Diagnostic, error) { + s.serverDiscoveryMutex.RLock() + server, ok := s.ephemeralResources[typeName] + discoveryComplete := s.serverDiscoveryComplete + s.serverDiscoveryMutex.RUnlock() + + if discoveryComplete { + if ok { + return server, s.serverDiscoveryDiagnostics, nil + } + + return nil, []*tfprotov5.Diagnostic{ + ephemeralResourceMissingError(typeName), + }, nil + } + + err := s.serverDiscovery(ctx) + + if err != nil || diagnosticsHasError(s.serverDiscoveryDiagnostics) { + return nil, s.serverDiscoveryDiagnostics, err + } + + s.serverDiscoveryMutex.RLock() + server, ok = s.ephemeralResources[typeName] + s.serverDiscoveryMutex.RUnlock() + + if !ok { + return nil, []*tfprotov5.Diagnostic{ + ephemeralResourceMissingError(typeName), + }, nil + } + + return server, s.serverDiscoveryDiagnostics, nil +} + func (s *muxServer) getFunctionServer(ctx context.Context, name string) (tfprotov5.ProviderServer, []*tfprotov5.Diagnostic, error) { s.serverDiscoveryMutex.RLock() server, ok := s.functions[name] @@ -163,7 +202,7 @@ func (s *muxServer) getResourceServer(ctx context.Context, typeName string) (tfp // serverDiscovery will populate the mux server "routing" for functions and // resource types by calling all underlying server GetMetadata RPC and falling // back to GetProviderSchema RPC. It is intended to only be called through -// getDataSourceServer, getFunctionServer, and getResourceServer. +// getDataSourceServer, getEphemeralResourceServer, getFunctionServer, and getResourceServer. // // The error return represents gRPC errors, which except for the GetMetadata // call returning the gRPC unimplemented error, is always returned. @@ -201,6 +240,16 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { s.dataSources[serverDataSource.TypeName] = server } + for _, serverEphemeralResource := range metadataResp.EphemeralResources { + if _, ok := s.ephemeralResources[serverEphemeralResource.TypeName]; ok { + s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, ephemeralResourceDuplicateError(serverEphemeralResource.TypeName)) + + continue + } + + s.ephemeralResources[serverEphemeralResource.TypeName] = server + } + for _, serverFunction := range metadataResp.Functions { if _, ok := s.functions[serverFunction.Name]; ok { s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, functionDuplicateError(serverFunction.Name)) @@ -253,6 +302,16 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { s.dataSources[typeName] = server } + for typeName := range providerSchemaResp.EphemeralResourceSchemas { + if _, ok := s.ephemeralResources[typeName]; ok { + s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, ephemeralResourceDuplicateError(typeName)) + + continue + } + + s.ephemeralResources[typeName] = server + } + for name := range providerSchemaResp.Functions { if _, ok := s.functions[name]; ok { s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, functionDuplicateError(name)) @@ -289,9 +348,11 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { // - Only one provider implements each managed resource // - Only one provider implements each data source // - Only one provider implements each function +// - Only one provider implements each ephemeral resource func NewMuxServer(_ context.Context, servers ...func() tfprotov5.ProviderServer) (*muxServer, error) { result := muxServer{ dataSources: make(map[string]tfprotov5.ProviderServer), + ephemeralResources: make(map[string]tfprotov5.ProviderServer), functions: make(map[string]tfprotov5.ProviderServer), resources: make(map[string]tfprotov5.ProviderServer), resourceCapabilities: make(map[string]*tfprotov5.ServerCapabilities), diff --git a/tf5muxserver/mux_server_CloseEphemeralResource.go b/tf5muxserver/mux_server_CloseEphemeralResource.go new file mode 100644 index 0000000..3023b42 --- /dev/null +++ b/tf5muxserver/mux_server_CloseEphemeralResource.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +func (s *muxServer) CloseEphemeralResource(ctx context.Context, req *tfprotov5.CloseEphemeralResourceRequest) (*tfprotov5.CloseEphemeralResourceResponse, error) { + rpc := "CloseEphemeralResource" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + server, diags, err := s.getEphemeralResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.CloseEphemeralResourceResponse{ + Diagnostics: diags, + }, nil + } + + // TODO: Remove and call server.CloseEphemeralResource below directly once interface becomes required. + ephemeralResourceServer, ok := server.(tfprotov5.EphemeralResourceServer) + if !ok { + resp := &tfprotov5.CloseEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "CloseEphemeralResource Not Implemented", + Detail: "A CloseEphemeralResource call was received by the provider, however the provider does not implement CloseEphemeralResource. " + + "Either upgrade the provider to a version that implements CloseEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return resp, nil + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return ephemeralResourceServer.CloseEphemeralResource(ctx, req) +} diff --git a/tf5muxserver/mux_server_CloseEphemeralResource_test.go b/tf5muxserver/mux_server_CloseEphemeralResource_test.go new file mode 100644 index 0000000..878fbb8 --- /dev/null +++ b/tf5muxserver/mux_server_CloseEphemeralResource_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerCloseEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + testServer1 := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource_server1": {}, + }, + }, + } + testServer2 := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource_server2": {}, + }, + }, + } + servers := []func() tfprotov5.ProviderServer{testServer1.ProviderServer, testServer2.ProviderServer} + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := muxServer.ProviderServer().(tfprotov5.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("muxServer should implement tfprotov5.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.CloseEphemeralResource(ctx, &tfprotov5.CloseEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !testServer1.CloseEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("expected test_ephemeral_resource_server1 CloseEphemeralResource to be called on server1") + } + + if testServer2.CloseEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("unexpected test_ephemeral_resource_server1 CloseEphemeralResource called on server2") + } + + _, err = ephemeralResourceServer.CloseEphemeralResource(ctx, &tfprotov5.CloseEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if testServer1.CloseEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("unexpected test_ephemeral_resource_server2 CloseEphemeralResource called on server1") + } + + if !testServer2.CloseEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("expected test_ephemeral_resource_server2 CloseEphemeralResource to be called on server2") + } +} diff --git a/tf5muxserver/mux_server_GetMetadata.go b/tf5muxserver/mux_server_GetMetadata.go index bd3e1b4..4e57e66 100644 --- a/tf5muxserver/mux_server_GetMetadata.go +++ b/tf5muxserver/mux_server_GetMetadata.go @@ -14,8 +14,8 @@ import ( // GetMetadata merges the metadata returned by the // tfprotov5.ProviderServers associated with muxServer into a single response. -// Resources and data sources must be returned from only one server or an error -// diagnostic is returned. +// Resources, data sources, ephemeral resources, and functions must be returned +// from only one server or an error diagnostic is returned. func (s *muxServer) GetMetadata(ctx context.Context, req *tfprotov5.GetMetadataRequest) (*tfprotov5.GetMetadataResponse, error) { rpc := "GetMetadata" ctx = logging.InitContext(ctx) @@ -26,6 +26,7 @@ func (s *muxServer) GetMetadata(ctx context.Context, req *tfprotov5.GetMetadataR resp := &tfprotov5.GetMetadataResponse{ DataSources: make([]tfprotov5.DataSourceMetadata, 0), + EphemeralResources: make([]tfprotov5.EphemeralResourceMetadata, 0), Functions: make([]tfprotov5.FunctionMetadata, 0), Resources: make([]tfprotov5.ResourceMetadata, 0), ServerCapabilities: serverCapabilities, @@ -54,6 +55,17 @@ func (s *muxServer) GetMetadata(ctx context.Context, req *tfprotov5.GetMetadataR resp.DataSources = append(resp.DataSources, datasource) } + for _, ephemeralResource := range serverResp.EphemeralResources { + if ephemeralResourceMetadataContainsTypeName(resp.EphemeralResources, ephemeralResource.TypeName) { + resp.Diagnostics = append(resp.Diagnostics, ephemeralResourceDuplicateError(ephemeralResource.TypeName)) + + continue + } + + s.ephemeralResources[ephemeralResource.TypeName] = server + resp.EphemeralResources = append(resp.EphemeralResources, ephemeralResource) + } + for _, function := range serverResp.Functions { if functionMetadataContainsName(resp.Functions, function.Name) { resp.Diagnostics = append(resp.Diagnostics, functionDuplicateError(function.Name)) @@ -91,6 +103,16 @@ func datasourceMetadataContainsTypeName(metadatas []tfprotov5.DataSourceMetadata return false } +func ephemeralResourceMetadataContainsTypeName(metadatas []tfprotov5.EphemeralResourceMetadata, typeName string) bool { + for _, metadata := range metadatas { + if typeName == metadata.TypeName { + return true + } + } + + return false +} + func functionMetadataContainsName(metadatas []tfprotov5.FunctionMetadata, name string) bool { for _, metadata := range metadatas { if name == metadata.Name { diff --git a/tf5muxserver/mux_server_GetMetadata_test.go b/tf5muxserver/mux_server_GetMetadata_test.go index d827db2..2e02960 100644 --- a/tf5muxserver/mux_server_GetMetadata_test.go +++ b/tf5muxserver/mux_server_GetMetadata_test.go @@ -21,6 +21,7 @@ func TestMuxServerGetMetadata(t *testing.T) { servers []func() tfprotov5.ProviderServer expectedDataSources []tfprotov5.DataSourceMetadata expectedDiagnostics []*tfprotov5.Diagnostic + expectedEphemeralResources []tfprotov5.EphemeralResourceMetadata expectedFunctions []tfprotov5.FunctionMetadata expectedResources []tfprotov5.ResourceMetadata expectedServerCapabilities *tfprotov5.ServerCapabilities @@ -47,6 +48,14 @@ func TestMuxServerGetMetadata(t *testing.T) { Name: "test_function1", }, }, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + { + TypeName: "test_foo", + }, + { + TypeName: "test_bar", + }, + }, }, }).ProviderServer, (&tf5testserver.TestServer{ @@ -72,6 +81,11 @@ func TestMuxServerGetMetadata(t *testing.T) { Name: "test_function3", }, }, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + { + TypeName: "test_quux", + }, + }, }, }).ProviderServer, }, @@ -108,6 +122,17 @@ func TestMuxServerGetMetadata(t *testing.T) { Name: "test_function3", }, }, + expectedEphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + { + TypeName: "test_foo", + }, + { + TypeName: "test_bar", + }, + { + TypeName: "test_quux", + }, + }, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -150,6 +175,52 @@ func TestMuxServerGetMetadata(t *testing.T) { "Duplicate data source type: test_foo", }, }, + expectedEphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov5.FunctionMetadata{}, + expectedResources: []tfprotov5.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + "duplicate-ephemeral-resource-type": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov5.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same ephemeral resource type across underlying providers. " + + "Ephemeral resource types must be implemented by only one underlying provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate ephemeral resource type: test_foo", + }, + }, + expectedEphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + { + TypeName: "test_foo", + }, + }, expectedFunctions: []tfprotov5.FunctionMetadata{}, expectedResources: []tfprotov5.ResourceMetadata{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ @@ -190,6 +261,7 @@ func TestMuxServerGetMetadata(t *testing.T) { "Duplicate function: test_function", }, }, + expectedEphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, expectedFunctions: []tfprotov5.FunctionMetadata{ { Name: "test_function", @@ -234,7 +306,8 @@ func TestMuxServerGetMetadata(t *testing.T) { "Duplicate resource type: test_foo", }, }, - expectedFunctions: []tfprotov5.FunctionMetadata{}, + expectedEphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov5.FunctionMetadata{}, expectedResources: []tfprotov5.ResourceMetadata{ { TypeName: "test_foo", @@ -272,8 +345,9 @@ func TestMuxServerGetMetadata(t *testing.T) { }, }).ProviderServer, }, - expectedDataSources: []tfprotov5.DataSourceMetadata{}, - expectedFunctions: []tfprotov5.FunctionMetadata{}, + expectedDataSources: []tfprotov5.DataSourceMetadata{}, + expectedEphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov5.FunctionMetadata{}, expectedResources: []tfprotov5.ResourceMetadata{ { TypeName: "test_with_server_capabilities", @@ -312,8 +386,9 @@ func TestMuxServerGetMetadata(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: []tfprotov5.FunctionMetadata{}, - expectedResources: []tfprotov5.ResourceMetadata{}, + expectedEphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov5.FunctionMetadata{}, + expectedResources: []tfprotov5.ResourceMetadata{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -359,8 +434,9 @@ func TestMuxServerGetMetadata(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: []tfprotov5.FunctionMetadata{}, - expectedResources: []tfprotov5.ResourceMetadata{}, + expectedEphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov5.FunctionMetadata{}, + expectedResources: []tfprotov5.ResourceMetadata{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -391,8 +467,9 @@ func TestMuxServerGetMetadata(t *testing.T) { Detail: "test warning details", }, }, - expectedFunctions: []tfprotov5.FunctionMetadata{}, - expectedResources: []tfprotov5.ResourceMetadata{}, + expectedEphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov5.FunctionMetadata{}, + expectedResources: []tfprotov5.ResourceMetadata{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -438,8 +515,9 @@ func TestMuxServerGetMetadata(t *testing.T) { Detail: "test warning details", }, }, - expectedFunctions: []tfprotov5.FunctionMetadata{}, - expectedResources: []tfprotov5.ResourceMetadata{}, + expectedEphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov5.FunctionMetadata{}, + expectedResources: []tfprotov5.ResourceMetadata{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -485,8 +563,9 @@ func TestMuxServerGetMetadata(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: []tfprotov5.FunctionMetadata{}, - expectedResources: []tfprotov5.ResourceMetadata{}, + expectedEphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov5.FunctionMetadata{}, + expectedResources: []tfprotov5.ResourceMetadata{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -521,6 +600,10 @@ func TestMuxServerGetMetadata(t *testing.T) { t.Errorf("diagnostics didn't match expectations: %s", diff) } + if diff := cmp.Diff(resp.EphemeralResources, testCase.expectedEphemeralResources); diff != "" { + t.Errorf("ephemeral resources didn't match expectations: %s", diff) + } + if diff := cmp.Diff(resp.Functions, testCase.expectedFunctions); diff != "" { t.Errorf("functions didn't match expectations: %s", diff) } diff --git a/tf5muxserver/mux_server_GetProviderSchema.go b/tf5muxserver/mux_server_GetProviderSchema.go index 19bfd87..70b2440 100644 --- a/tf5muxserver/mux_server_GetProviderSchema.go +++ b/tf5muxserver/mux_server_GetProviderSchema.go @@ -14,8 +14,8 @@ import ( // GetProviderSchema merges the schemas returned by the // tfprotov5.ProviderServers associated with muxServer into a single schema. -// Resources and data sources must be returned from only one server. Provider -// and ProviderMeta schemas must be identical between all servers. +// Resources, data sources, ephemeral resources, and functions must be returned +// from only one server. Provider and ProviderMeta schemas must be identical between all servers. func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) { rpc := "GetProviderSchema" ctx = logging.InitContext(ctx) @@ -25,10 +25,11 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetPro defer s.serverDiscoveryMutex.Unlock() resp := &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: make(map[string]*tfprotov5.Schema), - Functions: make(map[string]*tfprotov5.Function), - ResourceSchemas: make(map[string]*tfprotov5.Schema), - ServerCapabilities: serverCapabilities, + DataSourceSchemas: make(map[string]*tfprotov5.Schema), + EphemeralResourceSchemas: make(map[string]*tfprotov5.Schema), + Functions: make(map[string]*tfprotov5.Function), + ResourceSchemas: make(map[string]*tfprotov5.Schema), + ServerCapabilities: serverCapabilities, } for _, server := range s.servers { @@ -106,6 +107,17 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetPro s.functions[name] = server resp.Functions[name] = definition } + + for ephemeralResourceType, schema := range serverResp.EphemeralResourceSchemas { + if _, ok := resp.EphemeralResourceSchemas[ephemeralResourceType]; ok { + resp.Diagnostics = append(resp.Diagnostics, ephemeralResourceDuplicateError(ephemeralResourceType)) + + continue + } + + s.ephemeralResources[ephemeralResourceType] = server + resp.EphemeralResourceSchemas[ephemeralResourceType] = schema + } } s.serverDiscoveryComplete = true diff --git a/tf5muxserver/mux_server_GetProviderSchema_test.go b/tf5muxserver/mux_server_GetProviderSchema_test.go index a7d1b84..2ee470f 100644 --- a/tf5muxserver/mux_server_GetProviderSchema_test.go +++ b/tf5muxserver/mux_server_GetProviderSchema_test.go @@ -19,14 +19,15 @@ func TestMuxServerGetProviderSchema(t *testing.T) { t.Parallel() testCases := map[string]struct { - servers []func() tfprotov5.ProviderServer - expectedDataSourceSchemas map[string]*tfprotov5.Schema - expectedDiagnostics []*tfprotov5.Diagnostic - expectedFunctions map[string]*tfprotov5.Function - expectedProviderSchema *tfprotov5.Schema - expectedProviderMetaSchema *tfprotov5.Schema - expectedResourceSchemas map[string]*tfprotov5.Schema - expectedServerCapabilities *tfprotov5.ServerCapabilities + servers []func() tfprotov5.ProviderServer + expectedDataSourceSchemas map[string]*tfprotov5.Schema + expectedDiagnostics []*tfprotov5.Diagnostic + expectedEphemeralResourcesSchemas map[string]*tfprotov5.Schema + expectedFunctions map[string]*tfprotov5.Function + expectedProviderSchema *tfprotov5.Schema + expectedProviderMetaSchema *tfprotov5.Schema + expectedResourceSchemas map[string]*tfprotov5.Schema + expectedServerCapabilities *tfprotov5.ServerCapabilities }{ "combined": { servers: []func() tfprotov5.ProviderServer{ @@ -152,6 +153,45 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, }, }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_foo": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "secret_number", + Type: tftypes.Number, + Required: true, + Description: "input the secret number", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + "test_ephemeral_bar": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "username", + Type: tftypes.String, + Optional: true, + Description: "your username", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "password", + Type: tftypes.String, + Optional: true, + Description: "your password", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + }, }, }).ProviderServer, (&tf5testserver.TestServer{ @@ -279,6 +319,23 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, }, }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_foobar": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "secret_number", + Type: tftypes.Number, + Computed: true, + Description: "A generated secret number", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + }, }, }).ProviderServer, }, @@ -462,6 +519,60 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, }, }, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_foo": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "secret_number", + Type: tftypes.Number, + Required: true, + Description: "input the secret number", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + "test_ephemeral_bar": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "username", + Type: tftypes.String, + Optional: true, + Description: "your username", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "password", + Type: tftypes.String, + Optional: true, + Description: "your password", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + "test_ephemeral_foobar": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "secret_number", + Type: tftypes.Number, + Computed: true, + Description: "A generated secret number", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + }, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -498,6 +609,46 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "Duplicate data source type: test_foo", }, }, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{}, + expectedFunctions: map[string]*tfprotov5.Function{}, + expectedResourceSchemas: map[string]*tfprotov5.Schema{}, + expectedServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + "duplicate-ephemeral-resource-type": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_foo": {}, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_foo": {}, + }, + }, + }).ProviderServer, + }, + expectedDataSourceSchemas: map[string]*tfprotov5.Schema{}, + expectedDiagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same ephemeral resource type across underlying providers. " + + "Ephemeral resource types must be implemented by only one underlying provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate ephemeral resource type: test_foo", + }, + }, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{ + "test_foo": {}, + }, expectedFunctions: map[string]*tfprotov5.Function{}, expectedResourceSchemas: map[string]*tfprotov5.Schema{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ @@ -534,6 +685,7 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "Duplicate function: test_function", }, }, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{}, expectedFunctions: map[string]*tfprotov5.Function{ "test_function": {}, }, @@ -572,7 +724,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "Duplicate resource type: test_foo", }, }, - expectedFunctions: map[string]*tfprotov5.Function{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{}, + expectedFunctions: map[string]*tfprotov5.Function{}, expectedResourceSchemas: map[string]*tfprotov5.Schema{ "test_foo": {}, }, @@ -649,7 +802,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { ), }, }, - expectedFunctions: map[string]*tfprotov5.Function{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{}, + expectedFunctions: map[string]*tfprotov5.Function{}, expectedProviderSchema: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -735,7 +889,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { ), }, }, - expectedFunctions: map[string]*tfprotov5.Function{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{}, + expectedFunctions: map[string]*tfprotov5.Function{}, expectedProviderMetaSchema: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -776,8 +931,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, }).ProviderServer, }, - expectedDataSourceSchemas: map[string]*tfprotov5.Schema{}, - expectedFunctions: map[string]*tfprotov5.Function{}, + expectedDataSourceSchemas: map[string]*tfprotov5.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{}, + expectedFunctions: map[string]*tfprotov5.Function{}, expectedResourceSchemas: map[string]*tfprotov5.Schema{ "test_with_server_capabilities": {}, "test_without_server_capabilities": {}, @@ -812,8 +968,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: map[string]*tfprotov5.Function{}, - expectedResourceSchemas: map[string]*tfprotov5.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{}, + expectedFunctions: map[string]*tfprotov5.Function{}, + expectedResourceSchemas: map[string]*tfprotov5.Schema{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -859,8 +1016,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: map[string]*tfprotov5.Function{}, - expectedResourceSchemas: map[string]*tfprotov5.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{}, + expectedFunctions: map[string]*tfprotov5.Function{}, + expectedResourceSchemas: map[string]*tfprotov5.Schema{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -891,8 +1049,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { Detail: "test warning details", }, }, - expectedFunctions: map[string]*tfprotov5.Function{}, - expectedResourceSchemas: map[string]*tfprotov5.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{}, + expectedFunctions: map[string]*tfprotov5.Function{}, + expectedResourceSchemas: map[string]*tfprotov5.Schema{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -938,8 +1097,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { Detail: "test warning details", }, }, - expectedFunctions: map[string]*tfprotov5.Function{}, - expectedResourceSchemas: map[string]*tfprotov5.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{}, + expectedFunctions: map[string]*tfprotov5.Function{}, + expectedResourceSchemas: map[string]*tfprotov5.Schema{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -985,8 +1145,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: map[string]*tfprotov5.Function{}, - expectedResourceSchemas: map[string]*tfprotov5.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov5.Schema{}, + expectedFunctions: map[string]*tfprotov5.Function{}, + expectedResourceSchemas: map[string]*tfprotov5.Schema{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -1021,6 +1182,10 @@ func TestMuxServerGetProviderSchema(t *testing.T) { t.Errorf("diagnostics didn't match expectations: %s", diff) } + if diff := cmp.Diff(resp.EphemeralResourceSchemas, testCase.expectedEphemeralResourcesSchemas); diff != "" { + t.Errorf("ephemeral resources schemas didn't match expectations: %s", diff) + } + if diff := cmp.Diff(resp.Functions, testCase.expectedFunctions); diff != "" { t.Errorf("functions didn't match expectations: %s", diff) } diff --git a/tf5muxserver/mux_server_OpenEphemeralResource.go b/tf5muxserver/mux_server_OpenEphemeralResource.go new file mode 100644 index 0000000..8367f7a --- /dev/null +++ b/tf5muxserver/mux_server_OpenEphemeralResource.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +func (s *muxServer) OpenEphemeralResource(ctx context.Context, req *tfprotov5.OpenEphemeralResourceRequest) (*tfprotov5.OpenEphemeralResourceResponse, error) { + rpc := "OpenEphemeralResource" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + server, diags, err := s.getEphemeralResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: diags, + }, nil + } + + // TODO: Remove and call server.OpenEphemeralResource below directly once interface becomes required. + ephemeralResourceServer, ok := server.(tfprotov5.EphemeralResourceServer) + if !ok { + resp := &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "OpenEphemeralResource Not Implemented", + Detail: "A OpenEphemeralResource call was received by the provider, however the provider does not implement OpenEphemeralResource. " + + "Either upgrade the provider to a version that implements OpenEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return resp, nil + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return ephemeralResourceServer.OpenEphemeralResource(ctx, req) +} diff --git a/tf5muxserver/mux_server_OpenEphemeralResource_test.go b/tf5muxserver/mux_server_OpenEphemeralResource_test.go new file mode 100644 index 0000000..c3a8371 --- /dev/null +++ b/tf5muxserver/mux_server_OpenEphemeralResource_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerOpenEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + testServer1 := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource_server1": {}, + }, + }, + } + testServer2 := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource_server2": {}, + }, + }, + } + servers := []func() tfprotov5.ProviderServer{testServer1.ProviderServer, testServer2.ProviderServer} + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := muxServer.ProviderServer().(tfprotov5.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("muxServer should implement tfprotov5.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.OpenEphemeralResource(ctx, &tfprotov5.OpenEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !testServer1.OpenEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("expected test_ephemeral_resource_server1 OpenEphemeralResource to be called on server1") + } + + if testServer2.OpenEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("unexpected test_ephemeral_resource_server1 OpenEphemeralResource called on server2") + } + + _, err = ephemeralResourceServer.OpenEphemeralResource(ctx, &tfprotov5.OpenEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if testServer1.OpenEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("unexpected test_ephemeral_resource_server2 OpenEphemeralResource called on server1") + } + + if !testServer2.OpenEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("expected test_ephemeral_resource_server2 OpenEphemeralResource to be called on server2") + } +} diff --git a/tf5muxserver/mux_server_RenewEphemeralResource.go b/tf5muxserver/mux_server_RenewEphemeralResource.go new file mode 100644 index 0000000..e5eb9c7 --- /dev/null +++ b/tf5muxserver/mux_server_RenewEphemeralResource.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +func (s *muxServer) RenewEphemeralResource(ctx context.Context, req *tfprotov5.RenewEphemeralResourceRequest) (*tfprotov5.RenewEphemeralResourceResponse, error) { + rpc := "RenewEphemeralResource" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + server, diags, err := s.getEphemeralResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.RenewEphemeralResourceResponse{ + Diagnostics: diags, + }, nil + } + + // TODO: Remove and call server.RenewEphemeralResource below directly once interface becomes required. + ephemeralResourceServer, ok := server.(tfprotov5.EphemeralResourceServer) + if !ok { + resp := &tfprotov5.RenewEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "RenewEphemeralResource Not Implemented", + Detail: "A RenewEphemeralResource call was received by the provider, however the provider does not implement RenewEphemeralResource. " + + "Either upgrade the provider to a version that implements RenewEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return resp, nil + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return ephemeralResourceServer.RenewEphemeralResource(ctx, req) +} diff --git a/tf5muxserver/mux_server_RenewEphemeralResource_test.go b/tf5muxserver/mux_server_RenewEphemeralResource_test.go new file mode 100644 index 0000000..ef1bc5c --- /dev/null +++ b/tf5muxserver/mux_server_RenewEphemeralResource_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerRenewEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + testServer1 := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource_server1": {}, + }, + }, + } + testServer2 := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource_server2": {}, + }, + }, + } + servers := []func() tfprotov5.ProviderServer{testServer1.ProviderServer, testServer2.ProviderServer} + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := muxServer.ProviderServer().(tfprotov5.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("muxServer should implement tfprotov5.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.RenewEphemeralResource(ctx, &tfprotov5.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !testServer1.RenewEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("expected test_ephemeral_resource_server1 RenewEphemeralResource to be called on server1") + } + + if testServer2.RenewEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("unexpected test_ephemeral_resource_server1 RenewEphemeralResource called on server2") + } + + _, err = ephemeralResourceServer.RenewEphemeralResource(ctx, &tfprotov5.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if testServer1.RenewEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("unexpected test_ephemeral_resource_server2 RenewEphemeralResource called on server1") + } + + if !testServer2.RenewEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("expected test_ephemeral_resource_server2 RenewEphemeralResource to be called on server2") + } +} diff --git a/tf5muxserver/mux_server_ValidateEphemeralResourceConfig.go b/tf5muxserver/mux_server_ValidateEphemeralResourceConfig.go new file mode 100644 index 0000000..1468f7e --- /dev/null +++ b/tf5muxserver/mux_server_ValidateEphemeralResourceConfig.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +func (s *muxServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov5.ValidateEphemeralResourceConfigRequest) (*tfprotov5.ValidateEphemeralResourceConfigResponse, error) { + rpc := "ValidateEphemeralResourceTypeConfig" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + server, diags, err := s.getEphemeralResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diags, + }, nil + } + + // TODO: Remove and call server.ValidateEphemeralResourceConfig below directly once interface becomes required. + ephemeralResourceServer, ok := server.(tfprotov5.EphemeralResourceServer) + if !ok { + resp := &tfprotov5.ValidateEphemeralResourceConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "ValidateEphemeralResourceConfig Not Implemented", + Detail: "A ValidateEphemeralResourceConfig call was received by the provider, however the provider does not implement ValidateEphemeralResourceConfig. " + + "Either upgrade the provider to a version that implements ValidateEphemeralResourceConfig or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return resp, nil + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return ephemeralResourceServer.ValidateEphemeralResourceConfig(ctx, req) +} diff --git a/tf5muxserver/mux_server_ValidateEphemeralResourceConfig_test.go b/tf5muxserver/mux_server_ValidateEphemeralResourceConfig_test.go new file mode 100644 index 0000000..2691869 --- /dev/null +++ b/tf5muxserver/mux_server_ValidateEphemeralResourceConfig_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerValidateEphemeralResourceConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + testServer1 := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource_server1": {}, + }, + }, + } + testServer2 := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource_server2": {}, + }, + }, + } + servers := []func() tfprotov5.ProviderServer{testServer1.ProviderServer, testServer2.ProviderServer} + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := muxServer.ProviderServer().(tfprotov5.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("muxServer should implement tfprotov5.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.ValidateEphemeralResourceConfig(ctx, &tfprotov5.ValidateEphemeralResourceConfigRequest{ + TypeName: "test_ephemeral_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !testServer1.ValidateEphemeralResourceConfigCalled["test_ephemeral_resource_server1"] { + t.Errorf("expected test_ephemeral_resource_server1 ValidateEphemeralResourceConfig to be called on server1") + } + + if testServer2.ValidateEphemeralResourceConfigCalled["test_ephemeral_resource_server1"] { + t.Errorf("unexpected test_ephemeral_resource_server1 ValidateEphemeralResourceConfig called on server2") + } + + _, err = ephemeralResourceServer.ValidateEphemeralResourceConfig(ctx, &tfprotov5.ValidateEphemeralResourceConfigRequest{ + TypeName: "test_ephemeral_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if testServer1.ValidateEphemeralResourceConfigCalled["test_ephemeral_resource_server2"] { + t.Errorf("unexpected test_ephemeral_resource_server2 ValidateEphemeralResourceConfig called on server1") + } + + if !testServer2.ValidateEphemeralResourceConfigCalled["test_ephemeral_resource_server2"] { + t.Errorf("expected test_ephemeral_resource_server2 ValidateEphemeralResourceConfig to be called on server2") + } +} diff --git a/tf5to6server/tf5to6server.go b/tf5to6server/tf5to6server.go index 2a81363..15ee11d 100644 --- a/tf5to6server/tf5to6server.go +++ b/tf5to6server/tf5to6server.go @@ -55,6 +55,35 @@ func (s v5tov6Server) CallFunction(ctx context.Context, req *tfprotov6.CallFunct return tfprotov5tov6.CallFunctionResponse(v5Resp), nil } +func (s v5tov6Server) CloseEphemeralResource(ctx context.Context, req *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) { + // TODO: Remove and call s.v5Server.CloseEphemeralResource below directly once interface becomes required + ephemeralResourceServer, ok := s.v5Server.(tfprotov5.EphemeralResourceServer) + if !ok { + v6Resp := &tfprotov6.CloseEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "CloseEphemeralResource Not Implemented", + Detail: "A CloseEphemeralResource call was received by the provider, however the provider does not implement the RPC. " + + "Either upgrade the provider to a version that implements CloseEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return v6Resp, nil + } + + v5Req := tfprotov6tov5.CloseEphemeralResourceRequest(req) + + // v5Resp, err := s.v5Server.CloseEphemeralResource(ctx, v5Req) + v5Resp, err := ephemeralResourceServer.CloseEphemeralResource(ctx, v5Req) + if err != nil { + return nil, err + } + + return tfprotov5tov6.CloseEphemeralResourceResponse(v5Resp), nil +} + func (s v5tov6Server) ConfigureProvider(ctx context.Context, req *tfprotov6.ConfigureProviderRequest) (*tfprotov6.ConfigureProviderResponse, error) { v5Req := tfprotov6tov5.ConfigureProviderRequest(req) v5Resp, err := s.v5Server.ConfigureProvider(ctx, v5Req) @@ -121,6 +150,35 @@ func (s v5tov6Server) MoveResourceState(ctx context.Context, req *tfprotov6.Move return tfprotov5tov6.MoveResourceStateResponse(v5Resp), nil } +func (s v5tov6Server) OpenEphemeralResource(ctx context.Context, req *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) { + // TODO: Remove and call s.v5Server.OpenEphemeralResource below directly once interface becomes required + ephemeralResourceServer, ok := s.v5Server.(tfprotov5.EphemeralResourceServer) + if !ok { + v6Resp := &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "OpenEphemeralResource Not Implemented", + Detail: "A OpenEphemeralResource call was received by the provider, however the provider does not implement the RPC. " + + "Either upgrade the provider to a version that implements OpenEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return v6Resp, nil + } + + v5Req := tfprotov6tov5.OpenEphemeralResourceRequest(req) + + // v5Resp, err := s.v5Server.OpenEphemeralResource(ctx, v5Req) + v5Resp, err := ephemeralResourceServer.OpenEphemeralResource(ctx, v5Req) + if err != nil { + return nil, err + } + + return tfprotov5tov6.OpenEphemeralResourceResponse(v5Resp), nil +} + func (s v5tov6Server) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { v5Req := tfprotov6tov5.PlanResourceChangeRequest(req) v5Resp, err := s.v5Server.PlanResourceChange(ctx, v5Req) @@ -159,6 +217,35 @@ func (s v5tov6Server) ReadResource(ctx context.Context, req *tfprotov6.ReadResou return tfprotov5tov6.ReadResourceResponse(v5Resp), nil } +func (s v5tov6Server) RenewEphemeralResource(ctx context.Context, req *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) { + // TODO: Remove and call s.v5Server.RenewEphemeralResource below directly once interface becomes required + ephemeralResourceServer, ok := s.v5Server.(tfprotov5.EphemeralResourceServer) + if !ok { + v6Resp := &tfprotov6.RenewEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "RenewEphemeralResource Not Implemented", + Detail: "A RenewEphemeralResource call was received by the provider, however the provider does not implement the RPC. " + + "Either upgrade the provider to a version that implements RenewEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return v6Resp, nil + } + + v5Req := tfprotov6tov5.RenewEphemeralResourceRequest(req) + + // v5Resp, err := s.v5Server.RenewEphemeralResource(ctx, v5Req) + v5Resp, err := ephemeralResourceServer.RenewEphemeralResource(ctx, v5Req) + if err != nil { + return nil, err + } + + return tfprotov5tov6.RenewEphemeralResourceResponse(v5Resp), nil +} + func (s v5tov6Server) StopProvider(ctx context.Context, req *tfprotov6.StopProviderRequest) (*tfprotov6.StopProviderResponse, error) { v5Req := tfprotov6tov5.StopProviderRequest(req) v5Resp, err := s.v5Server.StopProvider(ctx, v5Req) @@ -192,6 +279,35 @@ func (s v5tov6Server) ValidateDataResourceConfig(ctx context.Context, req *tfpro return tfprotov5tov6.ValidateDataResourceConfigResponse(v5Resp), nil } +func (s v5tov6Server) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { + // TODO: Remove and call s.v5Server.ValidateEphemeralResourceConfig below directly once interface becomes required + ephemeralResourceServer, ok := s.v5Server.(tfprotov5.EphemeralResourceServer) + if !ok { + v6Resp := &tfprotov6.ValidateEphemeralResourceConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "ValidateEphemeralResourceConfig Not Implemented", + Detail: "A ValidateEphemeralResourceConfig call was received by the provider, however the provider does not implement the RPC. " + + "Either upgrade the provider to a version that implements ValidateEphemeralResourceConfig or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return v6Resp, nil + } + + v5Req := tfprotov6tov5.ValidateEphemeralResourceConfigRequest(req) + + // v5Resp, err := s.v5Server.ValidateEphemeralResourceConfig(ctx, v5Req) + v5Resp, err := ephemeralResourceServer.ValidateEphemeralResourceConfig(ctx, v5Req) + if err != nil { + return nil, err + } + + return tfprotov5tov6.ValidateEphemeralResourceConfigResponse(v5Resp), nil +} + func (s v5tov6Server) ValidateProviderConfig(ctx context.Context, req *tfprotov6.ValidateProviderConfigRequest) (*tfprotov6.ValidateProviderConfigResponse, error) { v5Req := tfprotov6tov5.PrepareProviderConfigRequest(req) v5Resp, err := s.v5Server.PrepareProviderConfig(ctx, v5Req) diff --git a/tf5to6server/tf5to6server_test.go b/tf5to6server/tf5to6server_test.go index 3296e18..b7cdcb6 100644 --- a/tf5to6server/tf5to6server_test.go +++ b/tf5to6server/tf5to6server_test.go @@ -29,6 +29,9 @@ func TestUpgradeServer(t *testing.T) { DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": {}, }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": {}, + }, Functions: map[string]*tfprotov5.Function{ "test_function": {}, }, @@ -191,6 +194,43 @@ func TestV6ToV5ServerCallFunction(t *testing.T) { } } +func TestV6ToV5ServerCloseEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v5server := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": {}, + }, + }, + } + + v6server, err := tf5to6server.UpgradeServer(context.Background(), v5server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := v6server.(tfprotov6.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("v6server should implement tfprotov6.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.CloseEphemeralResource(ctx, &tfprotov6.CloseEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v5server.CloseEphemeralResourceCalled["test_ephemeral_resource"] { + t.Errorf("expected test_ephemeral_resource CloseEphemeralResource to be called") + } +} + func TestV6ToV5ServerConfigureProvider(t *testing.T) { t.Parallel() @@ -371,6 +411,43 @@ func TestV5ToV6ServerMoveResourceState(t *testing.T) { } } +func TestV6ToV5ServerOpenEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v5server := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": {}, + }, + }, + } + + v6server, err := tf5to6server.UpgradeServer(context.Background(), v5server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := v6server.(tfprotov6.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("v6server should implement tfprotov6.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.OpenEphemeralResource(ctx, &tfprotov6.OpenEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v5server.OpenEphemeralResourceCalled["test_ephemeral_resource"] { + t.Errorf("expected test_ephemeral_resource OpenEphemeralResource to be called") + } +} + func TestV6ToV5ServerPlanResourceChange(t *testing.T) { t.Parallel() @@ -464,6 +541,43 @@ func TestV6ToV5ServerReadResource(t *testing.T) { } } +func TestV6ToV5ServerRenewEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v5server := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": {}, + }, + }, + } + + v6server, err := tf5to6server.UpgradeServer(context.Background(), v5server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := v6server.(tfprotov6.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("v6server should implement tfprotov6.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.RenewEphemeralResource(ctx, &tfprotov6.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v5server.RenewEphemeralResourceCalled["test_ephemeral_resource"] { + t.Errorf("expected test_ephemeral_resource RenewEphemeralResource to be called") + } +} + func TestV6ToV5ServerStopProvider(t *testing.T) { t.Parallel() @@ -555,6 +669,43 @@ func TestV6ToV5ServerValidateDataResourceConfig(t *testing.T) { } } +func TestV6ToV5ServerValidateEphemeralResourceConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v5server := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource": {}, + }, + }, + } + + v6server, err := tf5to6server.UpgradeServer(context.Background(), v5server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := v6server.(tfprotov6.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("v6server should implement tfprotov6.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.ValidateEphemeralResourceConfig(ctx, &tfprotov6.ValidateEphemeralResourceConfigRequest{ + TypeName: "test_resource", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v5server.ValidateEphemeralResourceConfigCalled["test_resource"] { + t.Errorf("expected test_resource ValidateEphemeralResourceConfig to be called") + } +} + func TestV6ToV5ServerValidateProviderConfig(t *testing.T) { t.Parallel() diff --git a/tf6muxserver/diagnostics.go b/tf6muxserver/diagnostics.go index 80572fc..9e4bb29 100644 --- a/tf6muxserver/diagnostics.go +++ b/tf6muxserver/diagnostics.go @@ -28,6 +28,27 @@ func dataSourceMissingError(typeName string) *tfprotov6.Diagnostic { } } +func ephemeralResourceDuplicateError(typeName string) *tfprotov6.Diagnostic { + return &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same ephemeral resource type across underlying providers. " + + "Ephemeral resource types must be implemented by only one underlying provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate ephemeral resource type: " + typeName, + } +} + +func ephemeralResourceMissingError(typeName string) *tfprotov6.Diagnostic { + return &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Ephemeral Resource Not Implemented", + Detail: "The combined provider does not implement the requested ephemeral resource type. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Missing ephemeral resource type: " + typeName, + } +} + func diagnosticsHasError(diagnostics []*tfprotov6.Diagnostic) bool { for _, diagnostic := range diagnostics { if diagnostic == nil { diff --git a/tf6muxserver/mux_server.go b/tf6muxserver/mux_server.go index 243f4c7..b83bb56 100644 --- a/tf6muxserver/mux_server.go +++ b/tf6muxserver/mux_server.go @@ -8,9 +8,10 @@ import ( "sync" "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-mux/internal/logging" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" ) var _ tfprotov6.ProviderServer = &muxServer{} @@ -22,6 +23,9 @@ type muxServer struct { // Routing for data source types dataSources map[string]tfprotov6.ProviderServer + // Routing for ephemeral resource types + ephemeralResources map[string]tfprotov6.ProviderServer + // Routing for functions functions map[string]tfprotov6.ProviderServer @@ -90,6 +94,41 @@ func (s *muxServer) getDataSourceServer(ctx context.Context, typeName string) (t return server, s.serverDiscoveryDiagnostics, nil } +func (s *muxServer) getEphemeralResourceServer(ctx context.Context, typeName string) (tfprotov6.ProviderServer, []*tfprotov6.Diagnostic, error) { + s.serverDiscoveryMutex.RLock() + server, ok := s.ephemeralResources[typeName] + discoveryComplete := s.serverDiscoveryComplete + s.serverDiscoveryMutex.RUnlock() + + if discoveryComplete { + if ok { + return server, s.serverDiscoveryDiagnostics, nil + } + + return nil, []*tfprotov6.Diagnostic{ + ephemeralResourceMissingError(typeName), + }, nil + } + + err := s.serverDiscovery(ctx) + + if err != nil || diagnosticsHasError(s.serverDiscoveryDiagnostics) { + return nil, s.serverDiscoveryDiagnostics, err + } + + s.serverDiscoveryMutex.RLock() + server, ok = s.ephemeralResources[typeName] + s.serverDiscoveryMutex.RUnlock() + + if !ok { + return nil, []*tfprotov6.Diagnostic{ + ephemeralResourceMissingError(typeName), + }, nil + } + + return server, s.serverDiscoveryDiagnostics, nil +} + func (s *muxServer) getFunctionServer(ctx context.Context, name string) (tfprotov6.ProviderServer, []*tfprotov6.Diagnostic, error) { s.serverDiscoveryMutex.RLock() server, ok := s.functions[name] @@ -163,7 +202,7 @@ func (s *muxServer) getResourceServer(ctx context.Context, typeName string) (tfp // serverDiscovery will populate the mux server "routing" for functions and // resource types by calling all underlying server GetMetadata RPC and falling // back to GetProviderSchema RPC. It is intended to only be called through -// getDataSourceServer, getFunctionServer, and getResourceServer. +// getDataSourceServer, getEphemeralResourceServer, getFunctionServer, and getResourceServer. // // The error return represents gRPC errors, which except for the GetMetadata // call returning the gRPC unimplemented error, is always returned. @@ -201,6 +240,16 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { s.dataSources[serverDataSource.TypeName] = server } + for _, serverEphemeralResource := range metadataResp.EphemeralResources { + if _, ok := s.ephemeralResources[serverEphemeralResource.TypeName]; ok { + s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, ephemeralResourceDuplicateError(serverEphemeralResource.TypeName)) + + continue + } + + s.ephemeralResources[serverEphemeralResource.TypeName] = server + } + for _, serverFunction := range metadataResp.Functions { if _, ok := s.functions[serverFunction.Name]; ok { s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, functionDuplicateError(serverFunction.Name)) @@ -253,6 +302,16 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { s.dataSources[typeName] = server } + for typeName := range providerSchemaResp.EphemeralResourceSchemas { + if _, ok := s.ephemeralResources[typeName]; ok { + s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, ephemeralResourceDuplicateError(typeName)) + + continue + } + + s.ephemeralResources[typeName] = server + } + for name := range providerSchemaResp.Functions { if _, ok := s.functions[name]; ok { s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, functionDuplicateError(name)) @@ -290,9 +349,11 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { // - Only one provider implements each managed resource // - Only one provider implements each data source // - Only one provider implements each function +// - Only one provider implements each ephemeral resource func NewMuxServer(_ context.Context, servers ...func() tfprotov6.ProviderServer) (*muxServer, error) { result := muxServer{ dataSources: make(map[string]tfprotov6.ProviderServer), + ephemeralResources: make(map[string]tfprotov6.ProviderServer), functions: make(map[string]tfprotov6.ProviderServer), resources: make(map[string]tfprotov6.ProviderServer), resourceCapabilities: make(map[string]*tfprotov6.ServerCapabilities), diff --git a/tf6muxserver/mux_server_CloseEphemeralResource.go b/tf6muxserver/mux_server_CloseEphemeralResource.go new file mode 100644 index 0000000..9671e35 --- /dev/null +++ b/tf6muxserver/mux_server_CloseEphemeralResource.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +func (s *muxServer) CloseEphemeralResource(ctx context.Context, req *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) { + rpc := "CloseEphemeralResource" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + server, diags, err := s.getEphemeralResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.CloseEphemeralResourceResponse{ + Diagnostics: diags, + }, nil + } + + // TODO: Remove and call server.CloseEphemeralResource below directly once interface becomes required. + ephemeralResourceServer, ok := server.(tfprotov6.EphemeralResourceServer) + if !ok { + resp := &tfprotov6.CloseEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "CloseEphemeralResource Not Implemented", + Detail: "A CloseEphemeralResource call was received by the provider, however the provider does not implement CloseEphemeralResource. " + + "Either upgrade the provider to a version that implements CloseEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return resp, nil + } + + ctx = logging.Tfprotov6ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return ephemeralResourceServer.CloseEphemeralResource(ctx, req) +} diff --git a/tf6muxserver/mux_server_CloseEphemeralResource_test.go b/tf6muxserver/mux_server_CloseEphemeralResource_test.go new file mode 100644 index 0000000..b0b8d13 --- /dev/null +++ b/tf6muxserver/mux_server_CloseEphemeralResource_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf6testserver" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" +) + +func TestMuxServerCloseEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + testServer1 := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource_server1": {}, + }, + }, + } + testServer2 := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource_server2": {}, + }, + }, + } + servers := []func() tfprotov6.ProviderServer{testServer1.ProviderServer, testServer2.ProviderServer} + muxServer, err := tf6muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := muxServer.ProviderServer().(tfprotov6.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("muxServer should implement tfprotov6.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.CloseEphemeralResource(ctx, &tfprotov6.CloseEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !testServer1.CloseEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("expected test_ephemeral_resource_server1 CloseEphemeralResource to be called on server1") + } + + if testServer2.CloseEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("unexpected test_ephemeral_resource_server1 CloseEphemeralResource called on server2") + } + + _, err = ephemeralResourceServer.CloseEphemeralResource(ctx, &tfprotov6.CloseEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if testServer1.CloseEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("unexpected test_ephemeral_resource_server2 CloseEphemeralResource called on server1") + } + + if !testServer2.CloseEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("expected test_ephemeral_resource_server2 CloseEphemeralResource to be called on server2") + } +} diff --git a/tf6muxserver/mux_server_GetMetadata.go b/tf6muxserver/mux_server_GetMetadata.go index a181654..8a2f1e5 100644 --- a/tf6muxserver/mux_server_GetMetadata.go +++ b/tf6muxserver/mux_server_GetMetadata.go @@ -26,6 +26,7 @@ func (s *muxServer) GetMetadata(ctx context.Context, req *tfprotov6.GetMetadataR resp := &tfprotov6.GetMetadataResponse{ DataSources: make([]tfprotov6.DataSourceMetadata, 0), + EphemeralResources: make([]tfprotov6.EphemeralResourceMetadata, 0), Functions: make([]tfprotov6.FunctionMetadata, 0), Resources: make([]tfprotov6.ResourceMetadata, 0), ServerCapabilities: serverCapabilities, @@ -54,6 +55,17 @@ func (s *muxServer) GetMetadata(ctx context.Context, req *tfprotov6.GetMetadataR resp.DataSources = append(resp.DataSources, datasource) } + for _, ephemeralResource := range serverResp.EphemeralResources { + if ephemeralResourceMetadataContainsTypeName(resp.EphemeralResources, ephemeralResource.TypeName) { + resp.Diagnostics = append(resp.Diagnostics, ephemeralResourceDuplicateError(ephemeralResource.TypeName)) + + continue + } + + s.ephemeralResources[ephemeralResource.TypeName] = server + resp.EphemeralResources = append(resp.EphemeralResources, ephemeralResource) + } + for _, function := range serverResp.Functions { if functionMetadataContainsName(resp.Functions, function.Name) { resp.Diagnostics = append(resp.Diagnostics, functionDuplicateError(function.Name)) @@ -91,6 +103,16 @@ func datasourceMetadataContainsTypeName(metadatas []tfprotov6.DataSourceMetadata return false } +func ephemeralResourceMetadataContainsTypeName(metadatas []tfprotov6.EphemeralResourceMetadata, typeName string) bool { + for _, metadata := range metadatas { + if typeName == metadata.TypeName { + return true + } + } + + return false +} + func functionMetadataContainsName(metadatas []tfprotov6.FunctionMetadata, name string) bool { for _, metadata := range metadatas { if name == metadata.Name { diff --git a/tf6muxserver/mux_server_GetMetadata_test.go b/tf6muxserver/mux_server_GetMetadata_test.go index ae46d1c..ffb02c9 100644 --- a/tf6muxserver/mux_server_GetMetadata_test.go +++ b/tf6muxserver/mux_server_GetMetadata_test.go @@ -21,6 +21,7 @@ func TestMuxServerGetMetadata(t *testing.T) { servers []func() tfprotov6.ProviderServer expectedDataSources []tfprotov6.DataSourceMetadata expectedDiagnostics []*tfprotov6.Diagnostic + expectedEphemeralResources []tfprotov6.EphemeralResourceMetadata expectedFunctions []tfprotov6.FunctionMetadata expectedResources []tfprotov6.ResourceMetadata expectedServerCapabilities *tfprotov6.ServerCapabilities @@ -47,6 +48,14 @@ func TestMuxServerGetMetadata(t *testing.T) { Name: "test_function1", }, }, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + { + TypeName: "test_foo", + }, + { + TypeName: "test_bar", + }, + }, }, }).ProviderServer, (&tf6testserver.TestServer{ @@ -72,6 +81,11 @@ func TestMuxServerGetMetadata(t *testing.T) { Name: "test_function3", }, }, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + { + TypeName: "test_quux", + }, + }, }, }).ProviderServer, }, @@ -108,6 +122,17 @@ func TestMuxServerGetMetadata(t *testing.T) { Name: "test_function3", }, }, + expectedEphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + { + TypeName: "test_foo", + }, + { + TypeName: "test_bar", + }, + { + TypeName: "test_quux", + }, + }, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -150,6 +175,52 @@ func TestMuxServerGetMetadata(t *testing.T) { "Duplicate data source type: test_foo", }, }, + expectedEphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov6.FunctionMetadata{}, + expectedResources: []tfprotov6.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + "duplicate-ephemeral-resource-type": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov6.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same ephemeral resource type across underlying providers. " + + "Ephemeral resource types must be implemented by only one underlying provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate ephemeral resource type: test_foo", + }, + }, + expectedEphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + { + TypeName: "test_foo", + }, + }, expectedFunctions: []tfprotov6.FunctionMetadata{}, expectedResources: []tfprotov6.ResourceMetadata{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ @@ -190,6 +261,7 @@ func TestMuxServerGetMetadata(t *testing.T) { "Duplicate function: test_function", }, }, + expectedEphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, expectedFunctions: []tfprotov6.FunctionMetadata{ { Name: "test_function", @@ -234,7 +306,8 @@ func TestMuxServerGetMetadata(t *testing.T) { "Duplicate resource type: test_foo", }, }, - expectedFunctions: []tfprotov6.FunctionMetadata{}, + expectedEphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov6.FunctionMetadata{}, expectedResources: []tfprotov6.ResourceMetadata{ { TypeName: "test_foo", @@ -272,8 +345,9 @@ func TestMuxServerGetMetadata(t *testing.T) { }, }).ProviderServer, }, - expectedDataSources: []tfprotov6.DataSourceMetadata{}, - expectedFunctions: []tfprotov6.FunctionMetadata{}, + expectedDataSources: []tfprotov6.DataSourceMetadata{}, + expectedEphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov6.FunctionMetadata{}, expectedResources: []tfprotov6.ResourceMetadata{ { TypeName: "test_with_server_capabilities", @@ -312,8 +386,9 @@ func TestMuxServerGetMetadata(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: []tfprotov6.FunctionMetadata{}, - expectedResources: []tfprotov6.ResourceMetadata{}, + expectedEphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov6.FunctionMetadata{}, + expectedResources: []tfprotov6.ResourceMetadata{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -359,8 +434,9 @@ func TestMuxServerGetMetadata(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: []tfprotov6.FunctionMetadata{}, - expectedResources: []tfprotov6.ResourceMetadata{}, + expectedEphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov6.FunctionMetadata{}, + expectedResources: []tfprotov6.ResourceMetadata{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -391,8 +467,9 @@ func TestMuxServerGetMetadata(t *testing.T) { Detail: "test warning details", }, }, - expectedFunctions: []tfprotov6.FunctionMetadata{}, - expectedResources: []tfprotov6.ResourceMetadata{}, + expectedEphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov6.FunctionMetadata{}, + expectedResources: []tfprotov6.ResourceMetadata{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -438,8 +515,9 @@ func TestMuxServerGetMetadata(t *testing.T) { Detail: "test warning details", }, }, - expectedFunctions: []tfprotov6.FunctionMetadata{}, - expectedResources: []tfprotov6.ResourceMetadata{}, + expectedEphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov6.FunctionMetadata{}, + expectedResources: []tfprotov6.ResourceMetadata{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -485,8 +563,9 @@ func TestMuxServerGetMetadata(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: []tfprotov6.FunctionMetadata{}, - expectedResources: []tfprotov6.ResourceMetadata{}, + expectedEphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + expectedFunctions: []tfprotov6.FunctionMetadata{}, + expectedResources: []tfprotov6.ResourceMetadata{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -525,6 +604,10 @@ func TestMuxServerGetMetadata(t *testing.T) { t.Errorf("functions didn't match expectations: %s", diff) } + if diff := cmp.Diff(resp.EphemeralResources, testCase.expectedEphemeralResources); diff != "" { + t.Errorf("ephemeral resources didn't match expectations: %s", diff) + } + if diff := cmp.Diff(resp.Resources, testCase.expectedResources); diff != "" { t.Errorf("resources didn't match expectations: %s", diff) } diff --git a/tf6muxserver/mux_server_GetProviderSchema.go b/tf6muxserver/mux_server_GetProviderSchema.go index e725dcc..4d0db7d 100644 --- a/tf6muxserver/mux_server_GetProviderSchema.go +++ b/tf6muxserver/mux_server_GetProviderSchema.go @@ -25,10 +25,11 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov6.GetPro defer s.serverDiscoveryMutex.Unlock() resp := &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: make(map[string]*tfprotov6.Schema), - Functions: make(map[string]*tfprotov6.Function), - ResourceSchemas: make(map[string]*tfprotov6.Schema), - ServerCapabilities: serverCapabilities, + DataSourceSchemas: make(map[string]*tfprotov6.Schema), + EphemeralResourceSchemas: make(map[string]*tfprotov6.Schema), + Functions: make(map[string]*tfprotov6.Function), + ResourceSchemas: make(map[string]*tfprotov6.Schema), + ServerCapabilities: serverCapabilities, } for _, server := range s.servers { @@ -106,6 +107,17 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov6.GetPro s.functions[name] = server resp.Functions[name] = definition } + + for ephemeralResourceType, schema := range serverResp.EphemeralResourceSchemas { + if _, ok := resp.EphemeralResourceSchemas[ephemeralResourceType]; ok { + resp.Diagnostics = append(resp.Diagnostics, ephemeralResourceDuplicateError(ephemeralResourceType)) + + continue + } + + s.ephemeralResources[ephemeralResourceType] = server + resp.EphemeralResourceSchemas[ephemeralResourceType] = schema + } } s.serverDiscoveryComplete = true diff --git a/tf6muxserver/mux_server_GetProviderSchema_test.go b/tf6muxserver/mux_server_GetProviderSchema_test.go index 8276fbf..346ba27 100644 --- a/tf6muxserver/mux_server_GetProviderSchema_test.go +++ b/tf6muxserver/mux_server_GetProviderSchema_test.go @@ -19,14 +19,15 @@ func TestMuxServerGetProviderSchema(t *testing.T) { t.Parallel() testCases := map[string]struct { - servers []func() tfprotov6.ProviderServer - expectedDataSourceSchemas map[string]*tfprotov6.Schema - expectedDiagnostics []*tfprotov6.Diagnostic - expectedFunctions map[string]*tfprotov6.Function - expectedProviderSchema *tfprotov6.Schema - expectedProviderMetaSchema *tfprotov6.Schema - expectedResourceSchemas map[string]*tfprotov6.Schema - expectedServerCapabilities *tfprotov6.ServerCapabilities + servers []func() tfprotov6.ProviderServer + expectedDataSourceSchemas map[string]*tfprotov6.Schema + expectedDiagnostics []*tfprotov6.Diagnostic + expectedEphemeralResourcesSchemas map[string]*tfprotov6.Schema + expectedFunctions map[string]*tfprotov6.Function + expectedProviderSchema *tfprotov6.Schema + expectedProviderMetaSchema *tfprotov6.Schema + expectedResourceSchemas map[string]*tfprotov6.Schema + expectedServerCapabilities *tfprotov6.ServerCapabilities }{ "combined": { servers: []func() tfprotov6.ProviderServer{ @@ -152,6 +153,45 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, }, }, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_foo": { + Version: 1, + Block: &tfprotov6.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "secret_number", + Type: tftypes.Number, + Required: true, + Description: "input the secret number", + DescriptionKind: tfprotov6.StringKindPlain, + }, + }, + }, + }, + "test_ephemeral_bar": { + Version: 1, + Block: &tfprotov6.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "username", + Type: tftypes.String, + Optional: true, + Description: "your username", + DescriptionKind: tfprotov6.StringKindPlain, + }, + { + Name: "password", + Type: tftypes.String, + Optional: true, + Description: "your password", + DescriptionKind: tfprotov6.StringKindPlain, + }, + }, + }, + }, + }, }, }).ProviderServer, (&tf6testserver.TestServer{ @@ -279,6 +319,23 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, }, }, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_foobar": { + Version: 1, + Block: &tfprotov6.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "secret_number", + Type: tftypes.Number, + Computed: true, + Description: "A generated secret number", + DescriptionKind: tfprotov6.StringKindPlain, + }, + }, + }, + }, + }, }, }).ProviderServer, }, @@ -462,6 +519,60 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, }, }, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_foo": { + Version: 1, + Block: &tfprotov6.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "secret_number", + Type: tftypes.Number, + Required: true, + Description: "input the secret number", + DescriptionKind: tfprotov6.StringKindPlain, + }, + }, + }, + }, + "test_ephemeral_bar": { + Version: 1, + Block: &tfprotov6.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "username", + Type: tftypes.String, + Optional: true, + Description: "your username", + DescriptionKind: tfprotov6.StringKindPlain, + }, + { + Name: "password", + Type: tftypes.String, + Optional: true, + Description: "your password", + DescriptionKind: tfprotov6.StringKindPlain, + }, + }, + }, + }, + "test_ephemeral_foobar": { + Version: 1, + Block: &tfprotov6.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "secret_number", + Type: tftypes.Number, + Computed: true, + Description: "A generated secret number", + DescriptionKind: tfprotov6.StringKindPlain, + }, + }, + }, + }, + }, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -498,6 +609,46 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "Duplicate data source type: test_foo", }, }, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{}, + expectedFunctions: map[string]*tfprotov6.Function{}, + expectedResourceSchemas: map[string]*tfprotov6.Schema{}, + expectedServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + "duplicate-ephemeral-resource-type": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_foo": {}, + }, + }, + }).ProviderServer, + (&tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_foo": {}, + }, + }, + }).ProviderServer, + }, + expectedDataSourceSchemas: map[string]*tfprotov6.Schema{}, + expectedDiagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same ephemeral resource type across underlying providers. " + + "Ephemeral resource types must be implemented by only one underlying provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate ephemeral resource type: test_foo", + }, + }, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{ + "test_foo": {}, + }, expectedFunctions: map[string]*tfprotov6.Function{}, expectedResourceSchemas: map[string]*tfprotov6.Schema{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ @@ -534,6 +685,7 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "Duplicate function: test_function", }, }, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{}, expectedFunctions: map[string]*tfprotov6.Function{ "test_function": {}, }, @@ -572,7 +724,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "Duplicate resource type: test_foo", }, }, - expectedFunctions: map[string]*tfprotov6.Function{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{}, + expectedFunctions: map[string]*tfprotov6.Function{}, expectedResourceSchemas: map[string]*tfprotov6.Schema{ "test_foo": {}, }, @@ -649,7 +802,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { ), }, }, - expectedFunctions: map[string]*tfprotov6.Function{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{}, + expectedFunctions: map[string]*tfprotov6.Function{}, expectedProviderSchema: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -735,7 +889,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { ), }, }, - expectedFunctions: map[string]*tfprotov6.Function{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{}, + expectedFunctions: map[string]*tfprotov6.Function{}, expectedProviderMetaSchema: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -762,7 +917,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "test_with_server_capabilities": {}, }, ServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }).ProviderServer, @@ -774,8 +930,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, }).ProviderServer, }, - expectedDataSourceSchemas: map[string]*tfprotov6.Schema{}, - expectedFunctions: map[string]*tfprotov6.Function{}, + expectedDataSourceSchemas: map[string]*tfprotov6.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{}, + expectedFunctions: map[string]*tfprotov6.Function{}, expectedResourceSchemas: map[string]*tfprotov6.Schema{ "test_with_server_capabilities": {}, "test_without_server_capabilities": {}, @@ -810,8 +967,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: map[string]*tfprotov6.Function{}, - expectedResourceSchemas: map[string]*tfprotov6.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{}, + expectedFunctions: map[string]*tfprotov6.Function{}, + expectedResourceSchemas: map[string]*tfprotov6.Schema{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -857,8 +1015,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: map[string]*tfprotov6.Function{}, - expectedResourceSchemas: map[string]*tfprotov6.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{}, + expectedFunctions: map[string]*tfprotov6.Function{}, + expectedResourceSchemas: map[string]*tfprotov6.Schema{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -889,8 +1048,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { Detail: "test warning details", }, }, - expectedFunctions: map[string]*tfprotov6.Function{}, - expectedResourceSchemas: map[string]*tfprotov6.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{}, + expectedFunctions: map[string]*tfprotov6.Function{}, + expectedResourceSchemas: map[string]*tfprotov6.Schema{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -936,8 +1096,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { Detail: "test warning details", }, }, - expectedFunctions: map[string]*tfprotov6.Function{}, - expectedResourceSchemas: map[string]*tfprotov6.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{}, + expectedFunctions: map[string]*tfprotov6.Function{}, + expectedResourceSchemas: map[string]*tfprotov6.Schema{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -983,8 +1144,9 @@ func TestMuxServerGetProviderSchema(t *testing.T) { Detail: "test error details", }, }, - expectedFunctions: map[string]*tfprotov6.Function{}, - expectedResourceSchemas: map[string]*tfprotov6.Schema{}, + expectedEphemeralResourcesSchemas: map[string]*tfprotov6.Schema{}, + expectedFunctions: map[string]*tfprotov6.Function{}, + expectedResourceSchemas: map[string]*tfprotov6.Schema{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -1019,6 +1181,10 @@ func TestMuxServerGetProviderSchema(t *testing.T) { t.Errorf("diagnostics didn't match expectations: %s", diff) } + if diff := cmp.Diff(resp.EphemeralResourceSchemas, testCase.expectedEphemeralResourcesSchemas); diff != "" { + t.Errorf("ephemeral resources schemas didn't match expectations: %s", diff) + } + if diff := cmp.Diff(resp.Functions, testCase.expectedFunctions); diff != "" { t.Errorf("functions didn't match expectations: %s", diff) } diff --git a/tf6muxserver/mux_server_OpenEphemeralResource.go b/tf6muxserver/mux_server_OpenEphemeralResource.go new file mode 100644 index 0000000..8576473 --- /dev/null +++ b/tf6muxserver/mux_server_OpenEphemeralResource.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +func (s *muxServer) OpenEphemeralResource(ctx context.Context, req *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) { + rpc := "OpenEphemeralResource" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + server, diags, err := s.getEphemeralResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: diags, + }, nil + } + + // TODO: Remove and call server.OpenEphemeralResource below directly once interface becomes required. + ephemeralResourceServer, ok := server.(tfprotov6.EphemeralResourceServer) + if !ok { + resp := &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "OpenEphemeralResource Not Implemented", + Detail: "A OpenEphemeralResource call was received by the provider, however the provider does not implement OpenEphemeralResource. " + + "Either upgrade the provider to a version that implements OpenEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return resp, nil + } + + ctx = logging.Tfprotov6ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return ephemeralResourceServer.OpenEphemeralResource(ctx, req) +} diff --git a/tf6muxserver/mux_server_OpenEphemeralResource_test.go b/tf6muxserver/mux_server_OpenEphemeralResource_test.go new file mode 100644 index 0000000..2c887e8 --- /dev/null +++ b/tf6muxserver/mux_server_OpenEphemeralResource_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf6testserver" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" +) + +func TestMuxServerOpenEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + testServer1 := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource_server1": {}, + }, + }, + } + testServer2 := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource_server2": {}, + }, + }, + } + servers := []func() tfprotov6.ProviderServer{testServer1.ProviderServer, testServer2.ProviderServer} + muxServer, err := tf6muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := muxServer.ProviderServer().(tfprotov6.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("muxServer should implement tfprotov6.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.OpenEphemeralResource(ctx, &tfprotov6.OpenEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !testServer1.OpenEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("expected test_ephemeral_resource_server1 OpenEphemeralResource to be called on server1") + } + + if testServer2.OpenEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("unexpected test_ephemeral_resource_server1 OpenEphemeralResource called on server2") + } + + _, err = ephemeralResourceServer.OpenEphemeralResource(ctx, &tfprotov6.OpenEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if testServer1.OpenEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("unexpected test_ephemeral_resource_server2 OpenEphemeralResource called on server1") + } + + if !testServer2.OpenEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("expected test_ephemeral_resource_server2 OpenEphemeralResource to be called on server2") + } +} diff --git a/tf6muxserver/mux_server_RenewEphemeralResource.go b/tf6muxserver/mux_server_RenewEphemeralResource.go new file mode 100644 index 0000000..322baa4 --- /dev/null +++ b/tf6muxserver/mux_server_RenewEphemeralResource.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +func (s *muxServer) RenewEphemeralResource(ctx context.Context, req *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) { + rpc := "RenewEphemeralResource" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + server, diags, err := s.getEphemeralResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.RenewEphemeralResourceResponse{ + Diagnostics: diags, + }, nil + } + + // TODO: Remove and call server.RenewEphemeralResource below directly once interface becomes required. + ephemeralResourceServer, ok := server.(tfprotov6.EphemeralResourceServer) + if !ok { + resp := &tfprotov6.RenewEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "RenewEphemeralResource Not Implemented", + Detail: "A RenewEphemeralResource call was received by the provider, however the provider does not implement RenewEphemeralResource. " + + "Either upgrade the provider to a version that implements RenewEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return resp, nil + } + + ctx = logging.Tfprotov6ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return ephemeralResourceServer.RenewEphemeralResource(ctx, req) +} diff --git a/tf6muxserver/mux_server_RenewEphemeralResource_test.go b/tf6muxserver/mux_server_RenewEphemeralResource_test.go new file mode 100644 index 0000000..5bdb67d --- /dev/null +++ b/tf6muxserver/mux_server_RenewEphemeralResource_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf6testserver" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" +) + +func TestMuxServerRenewEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + testServer1 := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource_server1": {}, + }, + }, + } + testServer2 := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource_server2": {}, + }, + }, + } + servers := []func() tfprotov6.ProviderServer{testServer1.ProviderServer, testServer2.ProviderServer} + muxServer, err := tf6muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := muxServer.ProviderServer().(tfprotov6.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("muxServer should implement tfprotov6.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.RenewEphemeralResource(ctx, &tfprotov6.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !testServer1.RenewEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("expected test_ephemeral_resource_server1 RenewEphemeralResource to be called on server1") + } + + if testServer2.RenewEphemeralResourceCalled["test_ephemeral_resource_server1"] { + t.Errorf("unexpected test_ephemeral_resource_server1 RenewEphemeralResource called on server2") + } + + _, err = ephemeralResourceServer.RenewEphemeralResource(ctx, &tfprotov6.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if testServer1.RenewEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("unexpected test_ephemeral_resource_server2 RenewEphemeralResource called on server1") + } + + if !testServer2.RenewEphemeralResourceCalled["test_ephemeral_resource_server2"] { + t.Errorf("expected test_ephemeral_resource_server2 RenewEphemeralResource to be called on server2") + } +} diff --git a/tf6muxserver/mux_server_ValidateEphemeralResourceConfig.go b/tf6muxserver/mux_server_ValidateEphemeralResourceConfig.go new file mode 100644 index 0000000..4ee5146 --- /dev/null +++ b/tf6muxserver/mux_server_ValidateEphemeralResourceConfig.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +func (s *muxServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { + rpc := "ValidateEphemeralResourceTypeConfig" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + server, diags, err := s.getEphemeralResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diags, + }, nil + } + + // TODO: Remove and call server.ValidateEphemeralResourceConfig below directly once interface becomes required. + ephemeralResourceServer, ok := server.(tfprotov6.EphemeralResourceServer) + if !ok { + resp := &tfprotov6.ValidateEphemeralResourceConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "ValidateEphemeralResourceConfig Not Implemented", + Detail: "A ValidateEphemeralResourceConfig call was received by the provider, however the provider does not implement ValidateEphemeralResourceConfig. " + + "Either upgrade the provider to a version that implements ValidateEphemeralResourceConfig or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return resp, nil + } + + ctx = logging.Tfprotov6ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return ephemeralResourceServer.ValidateEphemeralResourceConfig(ctx, req) +} diff --git a/tf6muxserver/mux_server_ValidateEphemeralResourceConfig_test.go b/tf6muxserver/mux_server_ValidateEphemeralResourceConfig_test.go new file mode 100644 index 0000000..424349f --- /dev/null +++ b/tf6muxserver/mux_server_ValidateEphemeralResourceConfig_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf6testserver" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" +) + +func TestMuxServerValidateEphemeralResourceConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + testServer1 := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource_server1": {}, + }, + }, + } + testServer2 := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource_server2": {}, + }, + }, + } + servers := []func() tfprotov6.ProviderServer{testServer1.ProviderServer, testServer2.ProviderServer} + muxServer, err := tf6muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := muxServer.ProviderServer().(tfprotov6.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("muxServer should implement tfprotov6.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.ValidateEphemeralResourceConfig(ctx, &tfprotov6.ValidateEphemeralResourceConfigRequest{ + TypeName: "test_ephemeral_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !testServer1.ValidateEphemeralResourceConfigCalled["test_ephemeral_resource_server1"] { + t.Errorf("expected test_ephemeral_resource_server1 ValidateEphemeralResourceConfig to be called on server1") + } + + if testServer2.ValidateEphemeralResourceConfigCalled["test_ephemeral_resource_server1"] { + t.Errorf("unexpected test_ephemeral_resource_server1 ValidateEphemeralResourceConfig called on server2") + } + + _, err = ephemeralResourceServer.ValidateEphemeralResourceConfig(ctx, &tfprotov6.ValidateEphemeralResourceConfigRequest{ + TypeName: "test_ephemeral_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if testServer1.ValidateEphemeralResourceConfigCalled["test_ephemeral_resource_server2"] { + t.Errorf("unexpected test_ephemeral_resource_server2 ValidateEphemeralResourceConfig called on server1") + } + + if !testServer2.ValidateEphemeralResourceConfigCalled["test_ephemeral_resource_server2"] { + t.Errorf("expected test_ephemeral_resource_server2 ValidateEphemeralResourceConfig to be called on server2") + } +} diff --git a/tf6to5server/tf6to5server.go b/tf6to5server/tf6to5server.go index ccd7ced..de724c7 100644 --- a/tf6to5server/tf6to5server.go +++ b/tf6to5server/tf6to5server.go @@ -67,6 +67,35 @@ func (s v6tov5Server) CallFunction(ctx context.Context, req *tfprotov5.CallFunct return tfprotov6tov5.CallFunctionResponse(v6Resp), nil } +func (s v6tov5Server) CloseEphemeralResource(ctx context.Context, req *tfprotov5.CloseEphemeralResourceRequest) (*tfprotov5.CloseEphemeralResourceResponse, error) { + // TODO: Remove and call s.v6Server.CloseEphemeralResource below directly once interface becomes required + ephemeralResourceServer, ok := s.v6Server.(tfprotov6.EphemeralResourceServer) + if !ok { + v5Resp := &tfprotov5.CloseEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "CloseEphemeralResource Not Implemented", + Detail: "A CloseEphemeralResource call was received by the provider, however the provider does not implement the RPC. " + + "Either upgrade the provider to a version that implements CloseEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return v5Resp, nil + } + + v6Req := tfprotov5tov6.CloseEphemeralResourceRequest(req) + + // v6Resp, err := s.v6Server.CloseEphemeralResource(ctx, v6Req) + v6Resp, err := ephemeralResourceServer.CloseEphemeralResource(ctx, v6Req) + if err != nil { + return nil, err + } + + return tfprotov6tov5.CloseEphemeralResourceResponse(v6Resp), nil +} + func (s v6tov5Server) ConfigureProvider(ctx context.Context, req *tfprotov5.ConfigureProviderRequest) (*tfprotov5.ConfigureProviderResponse, error) { v6Req := tfprotov5tov6.ConfigureProviderRequest(req) v6Resp, err := s.v6Server.ConfigureProvider(ctx, v6Req) @@ -133,6 +162,35 @@ func (s v6tov5Server) MoveResourceState(ctx context.Context, req *tfprotov5.Move return tfprotov6tov5.MoveResourceStateResponse(v6Resp), nil } +func (s v6tov5Server) OpenEphemeralResource(ctx context.Context, req *tfprotov5.OpenEphemeralResourceRequest) (*tfprotov5.OpenEphemeralResourceResponse, error) { + // TODO: Remove and call s.v6Server.OpenEphemeralResource below directly once interface becomes required + ephemeralResourceServer, ok := s.v6Server.(tfprotov6.EphemeralResourceServer) + if !ok { + v5Resp := &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "OpenEphemeralResource Not Implemented", + Detail: "A OpenEphemeralResource call was received by the provider, however the provider does not implement the RPC. " + + "Either upgrade the provider to a version that implements OpenEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return v5Resp, nil + } + + v6Req := tfprotov5tov6.OpenEphemeralResourceRequest(req) + + // v6Resp, err := s.v6Server.OpenEphemeralResource(ctx, v6Req) + v6Resp, err := ephemeralResourceServer.OpenEphemeralResource(ctx, v6Req) + if err != nil { + return nil, err + } + + return tfprotov6tov5.OpenEphemeralResourceResponse(v6Resp), nil +} + func (s v6tov5Server) PlanResourceChange(ctx context.Context, req *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) { v6Req := tfprotov5tov6.PlanResourceChangeRequest(req) v6Resp, err := s.v6Server.PlanResourceChange(ctx, v6Req) @@ -182,6 +240,35 @@ func (s v6tov5Server) ReadResource(ctx context.Context, req *tfprotov5.ReadResou return tfprotov6tov5.ReadResourceResponse(v6Resp), nil } +func (s v6tov5Server) RenewEphemeralResource(ctx context.Context, req *tfprotov5.RenewEphemeralResourceRequest) (*tfprotov5.RenewEphemeralResourceResponse, error) { + // TODO: Remove and call s.v6Server.RenewEphemeralResource below directly once interface becomes required + ephemeralResourceServer, ok := s.v6Server.(tfprotov6.EphemeralResourceServer) + if !ok { + v5Resp := &tfprotov5.RenewEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "RenewEphemeralResource Not Implemented", + Detail: "A RenewEphemeralResource call was received by the provider, however the provider does not implement the RPC. " + + "Either upgrade the provider to a version that implements RenewEphemeralResource or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return v5Resp, nil + } + + v6Req := tfprotov5tov6.RenewEphemeralResourceRequest(req) + + // v6Resp, err := s.v6Server.RenewEphemeralResource(ctx, v6Req) + v6Resp, err := ephemeralResourceServer.RenewEphemeralResource(ctx, v6Req) + if err != nil { + return nil, err + } + + return tfprotov6tov5.RenewEphemeralResourceResponse(v6Resp), nil +} + func (s v6tov5Server) StopProvider(ctx context.Context, req *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) { v6Req := tfprotov5tov6.StopProviderRequest(req) v6Resp, err := s.v6Server.StopProvider(ctx, v6Req) @@ -215,6 +302,35 @@ func (s v6tov5Server) ValidateDataSourceConfig(ctx context.Context, req *tfproto return tfprotov6tov5.ValidateDataSourceConfigResponse(v6Resp), nil } +func (s v6tov5Server) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov5.ValidateEphemeralResourceConfigRequest) (*tfprotov5.ValidateEphemeralResourceConfigResponse, error) { + // TODO: Remove and call s.v6Server.ValidateEphemeralResourceConfig below directly once interface becomes required + ephemeralResourceServer, ok := s.v6Server.(tfprotov6.EphemeralResourceServer) + if !ok { + v5Resp := &tfprotov5.ValidateEphemeralResourceConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "ValidateEphemeralResourceConfig Not Implemented", + Detail: "A ValidateEphemeralResourceConfig call was received by the provider, however the provider does not implement the RPC. " + + "Either upgrade the provider to a version that implements ValidateEphemeralResourceConfig or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return v5Resp, nil + } + + v6Req := tfprotov5tov6.ValidateEphemeralResourceConfigRequest(req) + + // v6Resp, err := s.v6Server.ValidateEphemeralResourceConfig(ctx, v6Req) + v6Resp, err := ephemeralResourceServer.ValidateEphemeralResourceConfig(ctx, v6Req) + if err != nil { + return nil, err + } + + return tfprotov6tov5.ValidateEphemeralResourceConfigResponse(v6Resp), nil +} + func (s v6tov5Server) ValidateResourceTypeConfig(ctx context.Context, req *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) { v6Req := tfprotov5tov6.ValidateResourceConfigRequest(req) v6Resp, err := s.v6Server.ValidateResourceConfig(ctx, v6Req) diff --git a/tf6to5server/tf6to5server_test.go b/tf6to5server/tf6to5server_test.go index 2316386..5b9e9f5 100644 --- a/tf6to5server/tf6to5server_test.go +++ b/tf6to5server/tf6to5server_test.go @@ -31,6 +31,9 @@ func TestDowngradeServer(t *testing.T) { DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": {}, }, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": {}, + }, Functions: map[string]*tfprotov6.Function{ "test_function": {}, }, @@ -289,6 +292,43 @@ func TestV6ToV5ServerCallFunction(t *testing.T) { } } +func TestV6ToV5ServerCloseEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v6server := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + ResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": {}, + }, + }, + } + + v5server, err := tf6to5server.DowngradeServer(context.Background(), v6server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := v5server.(tfprotov5.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("v5server should implement tfprotov5.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.CloseEphemeralResource(ctx, &tfprotov5.CloseEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v6server.CloseEphemeralResourceCalled["test_ephemeral_resource"] { + t.Errorf("expected test_ephemeral_resource CloseEphemeralResource to be called") + } +} + func TestV6ToV5ServerConfigureProvider(t *testing.T) { t.Parallel() @@ -469,6 +509,43 @@ func TestV6ToV5ServerMoveResourceState(t *testing.T) { } } +func TestV6ToV5ServerOpenEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v6server := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + ResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": {}, + }, + }, + } + + v5server, err := tf6to5server.DowngradeServer(context.Background(), v6server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := v5server.(tfprotov5.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("v5server should implement tfprotov5.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.OpenEphemeralResource(ctx, &tfprotov5.OpenEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v6server.OpenEphemeralResourceCalled["test_ephemeral_resource"] { + t.Errorf("expected test_ephemeral_resource OpenEphemeralResource to be called") + } +} + func TestV6ToV5ServerPlanResourceChange(t *testing.T) { t.Parallel() @@ -591,6 +668,43 @@ func TestV6ToV5ServerReadResource(t *testing.T) { } } +func TestV6ToV5ServerRenewEphemeralResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v6server := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + ResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": {}, + }, + }, + } + + v5server, err := tf6to5server.DowngradeServer(context.Background(), v6server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := v5server.(tfprotov5.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("v5server should implement tfprotov5.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.RenewEphemeralResource(ctx, &tfprotov5.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v6server.RenewEphemeralResourceCalled["test_ephemeral_resource"] { + t.Errorf("expected test_ephemeral_resource RenewEphemeralResource to be called") + } +} + func TestV6ToV5ServerStopProvider(t *testing.T) { t.Parallel() @@ -682,6 +796,43 @@ func TestV6ToV5ServerValidateDataSourceConfig(t *testing.T) { } } +func TestV6ToV5ServerValidateEphemeralResourceConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v6server := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + ResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": {}, + }, + }, + } + + v5server, err := tf6to5server.DowngradeServer(context.Background(), v6server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + //nolint:staticcheck // Intentionally verifying interface implementation + ephemeralResourceServer, ok := v5server.(tfprotov5.ProviderServerWithEphemeralResources) + if !ok { + t.Fatal("v5server should implement tfprotov5.ProviderServerWithEphemeralResources") + } + + _, err = ephemeralResourceServer.ValidateEphemeralResourceConfig(ctx, &tfprotov5.ValidateEphemeralResourceConfigRequest{ + TypeName: "test_ephemeral_resource", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v6server.ValidateEphemeralResourceConfigCalled["test_ephemeral_resource"] { + t.Errorf("expected test_ephemeral_resource ValidateEphemeralResourceConfig to be called") + } +} + func TestV6ToV5ServerValidateResourceTypeConfig(t *testing.T) { t.Parallel()