Feature branch: Ephemeral resources (#2852)

Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
This commit is contained in:
Andrei Ciobanu
2025-08-04 16:39:12 +03:00
committed by GitHub
parent 1d38fd69d8
commit 4077c3d84f
166 changed files with 8044 additions and 565 deletions

View File

@@ -8,12 +8,16 @@ package plugin
import (
"bytes"
"fmt"
"slices"
"strings"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty/cty"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/legacy/hcl2shim"
@@ -96,6 +100,20 @@ func providerProtoSchema() *proto.GetProviderSchema_Response {
},
},
},
EphemeralResourceSchemas: map[string]*proto.Schema{
"eph": {
Version: 1,
Block: &proto.Schema_Block{
Attributes: []*proto.Schema_Attribute{
{
Name: "attr",
Type: []byte(`"string"`),
Required: true,
},
},
},
},
},
Functions: map[string]*proto.Function{
"fn": &proto.Function{
Parameters: []*proto.Function_Parameter{{
@@ -125,6 +143,22 @@ func TestGRPCProvider_GetSchema(t *testing.T) {
resp := p.GetProviderSchema(t.Context())
checkDiags(t, resp.Diagnostics)
{ // check ephemeral attribute of the schema blocks
if !resp.Provider.Block.Ephemeral {
t.Errorf("provider.Block.Ephemeral meant to be true")
}
checkResources := func(t *testing.T, r map[string]providers.Schema, want bool) {
for typ, schema := range r {
if schema.Block.Ephemeral != want {
t.Errorf("expected resource %q to have ephemeral as %t", typ, want)
}
}
}
checkResources(t, resp.ResourceTypes, false)
checkResources(t, resp.DataSources, false)
checkResources(t, resp.EphemeralResources, true)
}
}
// Ensure that gRPC errors are returned early.
@@ -327,6 +361,25 @@ func TestGRPCProvider_ValidateDataSourceConfig(t *testing.T) {
checkDiags(t, resp.Diagnostics)
}
func TestGRPCProvider_ValidateEphemeralResourceConfig(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
}
client.EXPECT().ValidateEphemeralResourceConfig(
gomock.Any(),
gomock.Any(),
).Return(&proto.ValidateEphemeralResourceConfig_Response{}, nil)
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
resp := p.ValidateEphemeralConfig(t.Context(), providers.ValidateEphemeralConfigRequest{
TypeName: "eph",
Config: cfg,
})
checkDiags(t, resp.Diagnostics)
}
func TestGRPCProvider_UpgradeResourceState(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
@@ -978,6 +1031,127 @@ func TestGRPCProvider_ReadDataSourceJSON(t *testing.T) {
}
}
func TestGRPCProvider_OpenEphemeralResource(t *testing.T) {
t.Run("success", func(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
}
future := time.Now().Add(time.Minute)
client.EXPECT().OpenEphemeralResource(
gomock.Any(),
gomock.Any(),
).Return(&proto.OpenEphemeralResource_Response{
Result: &proto.DynamicValue{
Msgpack: []byte("\x81\xa4attr\xa3bar"),
},
Private: []byte("private data"),
RenewAt: timestamppb.New(future),
Deferred: &proto.Deferred{
Reason: proto.Deferred_RESOURCE_CONFIG_UNKNOWN,
},
}, nil)
resp := p.OpenEphemeralResource(t.Context(), providers.OpenEphemeralResourceRequest{
TypeName: "eph",
Config: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("foo"),
}),
})
checkDiags(t, resp.Diagnostics)
expected := cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
})
if diff := cmp.Diff(expected, resp.Result, typeComparer, valueComparer, equateEmpty); diff != "" {
t.Fatalf("expected to have no diff between the expected result and result from the openEphemeral. got: %s", diff)
}
if resp.RenewAt == nil || !future.Equal(*resp.RenewAt) {
t.Fatalf("unexpected renewAt. got: %s, want %s", resp.RenewAt, future)
}
if got, want := resp.Private, []byte("private data"); !slices.Equal(got, want) {
t.Fatalf("unexpected private data. got: %q, want %q", got, want)
}
{
if resp.Deferred == nil {
t.Fatal("expected to have a deferred object but got none")
}
if got, want := resp.Deferred.DeferralReason, providers.DeferredBecauseResourceConfigUnknown; got != want {
t.Fatalf("unexpected deferred reason. got: %d, want %d", got, want)
}
}
})
t.Run("requested type is not in schema", func(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
}
resp := p.OpenEphemeralResource(t.Context(), providers.OpenEphemeralResourceRequest{
TypeName: "non_existing",
Config: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("foo"),
}),
})
checkDiagsHasError(t, resp.Diagnostics)
if got, want := resp.Diagnostics.Err().Error(), `unknown ephemeral resource "non_existing"`; !strings.Contains(got, want) {
t.Fatalf("diagnostis does not contain the expected content. got: %s; want: %s", got, want)
}
})
}
func TestGRPCProvider_RenewEphemeralResource(t *testing.T) {
ctrl := gomock.NewController(t)
client := mockproto.NewMockProviderClient(ctrl)
p := &GRPCProvider{
client: client,
}
future := time.Now().Add(time.Minute)
client.EXPECT().RenewEphemeralResource(
gomock.Any(),
gomock.Any(),
).Return(&proto.RenewEphemeralResource_Response{
Private: []byte("private data new"),
RenewAt: timestamppb.New(future),
}, nil)
resp := p.RenewEphemeralResource(t.Context(), providers.RenewEphemeralResourceRequest{
TypeName: "eph",
})
checkDiags(t, resp.Diagnostics)
if resp.RenewAt == nil || !future.Equal(*resp.RenewAt) {
t.Fatalf("unexpected renewAt. got: %s, want %s", resp.RenewAt, future)
}
if got, want := resp.Private, []byte("private data new"); !slices.Equal(got, want) {
t.Fatalf("unexpected private data. got: %q, want %q", got, want)
}
}
func TestGRPCProvider_CloseEphemeralResource(t *testing.T) {
ctrl := gomock.NewController(t)
client := mockproto.NewMockProviderClient(ctrl)
p := &GRPCProvider{
client: client,
}
client.EXPECT().CloseEphemeralResource(
gomock.Any(),
gomock.Any(),
).Return(&proto.CloseEphemeralResource_Response{}, nil)
resp := p.CloseEphemeralResource(t.Context(), providers.CloseEphemeralResourceRequest{
TypeName: "eph",
})
checkDiags(t, resp.Diagnostics)
}
func TestGRPCProvider_CallFunction(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{