Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.17/ENHANCEMENTS-20260727-182840.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: Add mock_provider support for ephemeral resources
time: 2026-07-27T18:28:40.309122+02:00
custom:
Issue: "38928"
103 changes: 96 additions & 7 deletions internal/configs/mock_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ func useForPlan(content *hcl.BodyContent, def bool) (bool, hcl.Diagnostics) {
// MockData packages up all the available mock and override data available to
// a mocked provider.
type MockData struct {
MockResources map[string]*MockResource
MockDataSources map[string]*MockResource
Overrides addrs.Map[addrs.Targetable, *Override]
MockResources map[string]*MockResource
MockDataSources map[string]*MockResource
MockEphemeralResources map[string]*MockResource
Overrides addrs.Map[addrs.Targetable, *Override]
}

// Merge will merge the target MockData object into the current MockData.
Expand Down Expand Up @@ -151,6 +152,24 @@ func (data *MockData) Merge(other *MockData, skipCollisions bool) (diags hcl.Dia
Subject: datasource.TypeRange.Ptr(),
})
}
for name, ephemeral := range other.MockEphemeralResources {
current, exists := data.MockEphemeralResources[name]
if !exists {
data.MockEphemeralResources[name] = ephemeral
continue
}

if skipCollisions {
continue
}

diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate mock resource block",
Detail: fmt.Sprintf("A mock_ephemeral %q block already exists at %s.", name, current.Range),
Subject: ephemeral.TypeRange.Ptr(),
})
}
for _, elem := range other.Overrides.Elems {
target, override := elem.Key, elem.Value

Expand Down Expand Up @@ -234,14 +253,15 @@ func decodeMockDataBody(body hcl.Body, useForPlanDefault bool, source OverrideSo
diags = append(diags, contentDiags...)

data := &MockData{
MockResources: make(map[string]*MockResource),
MockDataSources: make(map[string]*MockResource),
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
MockResources: make(map[string]*MockResource),
MockDataSources: make(map[string]*MockResource),
MockEphemeralResources: make(map[string]*MockResource),
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
}

for _, block := range content.Blocks {
switch block.Type {
case "mock_resource", "mock_data":
case "mock_resource", "mock_data", "mock_ephemeral":
resource, resourceDiags := decodeMockResourceBlock(block, useForPlanDefault)
diags = append(diags, resourceDiags...)

Expand Down Expand Up @@ -269,6 +289,17 @@ func decodeMockDataBody(body hcl.Body, useForPlanDefault bool, source OverrideSo
continue
}
data.MockDataSources[resource.Type] = resource
case addrs.EphemeralResourceMode:
if previous, ok := data.MockEphemeralResources[resource.Type]; ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate mock_ephemeral block",
Detail: fmt.Sprintf("A mock_ephemeral block for %s has already been defined at %s.", resource.Type, previous.Range),
Subject: resource.TypeRange.Ptr(),
})
continue
}
data.MockEphemeralResources[resource.Type] = resource
}
}
case "override_resource":
Expand Down Expand Up @@ -305,6 +336,23 @@ func decodeMockDataBody(body hcl.Body, useForPlanDefault bool, source OverrideSo
}
data.Overrides.Put(subject, override)
}
case "override_ephemeral":
override, overrideDiags := decodeOverrideEphemeralBlock(block, useForPlanDefault, source)
diags = append(diags, overrideDiags...)

if override != nil && override.Target != nil {
subject := override.Target.Subject
if previous, ok := data.Overrides.GetOk(subject); ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate override_ephemeral block",
Detail: fmt.Sprintf("An override_ephemeral block targeting %s has already been defined at %s.", subject, previous.Range),
Subject: override.Range.Ptr(),
})
continue
}
data.Overrides.Put(subject, override)
}
}
}

Expand All @@ -328,6 +376,8 @@ func decodeMockResourceBlock(block *hcl.Block, useForPlanDefault bool) (*MockRes
resource.Mode = addrs.ManagedResourceMode
case "mock_data":
resource.Mode = addrs.DataResourceMode
case "mock_ephemeral":
resource.Mode = addrs.EphemeralResourceMode
}

if defaults, exists := content.Attributes["defaults"]; exists {
Expand Down Expand Up @@ -404,6 +454,43 @@ func decodeOverrideResourceBlock(block *hcl.Block, useForPlanDefault bool, sourc
return override, diags
}

func decodeOverrideEphemeralBlock(block *hcl.Block, useForPlanDefault bool, source OverrideSource) (*Override, hcl.Diagnostics) {
override, diags := decodeOverrideBlock(block, "values", "override_ephemeral", useForPlanDefault, source)

if override.Target != nil {
var mode addrs.ResourceMode

switch override.Target.Subject.AddrType() {
case addrs.AbsResourceInstanceAddrType:
subject := override.Target.Subject.(addrs.AbsResourceInstance)
mode = subject.Resource.Resource.Mode
case addrs.AbsResourceAddrType:
subject := override.Target.Subject.(addrs.AbsResource)
mode = subject.Resource.Mode
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid override target",
Detail: fmt.Sprintf("You can only target ephemeral resources from override_ephemeral blocks, not %s.", override.Target.Subject),
Subject: override.TargetRange.Ptr(),
})
return nil, diags
}

if mode != addrs.EphemeralResourceMode {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid override target",
Detail: fmt.Sprintf("You can only target ephemeral resources from override_ephemeral blocks, not %s.", override.Target.Subject),
Subject: override.TargetRange.Ptr(),
})
return nil, diags
}
}

return override, diags
}

func decodeOverrideDataBlock(block *hcl.Block, useForPlanDefault bool, source OverrideSource) (*Override, hcl.Diagnostics) {
override, diags := decodeOverrideBlock(block, "values", "override_data", useForPlanDefault, source)

Expand Down Expand Up @@ -512,8 +599,10 @@ var mockDataSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{Type: "mock_resource", LabelNames: []string{"type"}},
{Type: "mock_data", LabelNames: []string{"type"}},
{Type: "mock_ephemeral", LabelNames: []string{"type"}},
{Type: "override_resource"},
{Type: "override_data"},
{Type: "override_ephemeral"},
},
}

Expand Down
100 changes: 100 additions & 0 deletions internal/configs/mock_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,87 @@ func TestMockData_Merge(t *testing.T) {
),
},
},
"ephemeral_no_collisions": {
current: &MockData{
MockResources: map[string]*MockResource{},
MockDataSources: map[string]*MockResource{},
MockEphemeralResources: map[string]*MockResource{
"test_ephemeral": {
Mode: addrs.EphemeralResourceMode,
Type: "test_ephemeral",
Defaults: cty.StringVal("current"),
},
},
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
},
target: &MockData{
MockResources: map[string]*MockResource{},
MockDataSources: map[string]*MockResource{},
MockEphemeralResources: map[string]*MockResource{
"test_ephemeral_two": {
Mode: addrs.EphemeralResourceMode,
Type: "test_ephemeral_two",
Defaults: cty.StringVal("target"),
},
},
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
},
result: &MockData{
MockResources: map[string]*MockResource{},
MockDataSources: map[string]*MockResource{},
MockEphemeralResources: map[string]*MockResource{
"test_ephemeral": {
Mode: addrs.EphemeralResourceMode,
Type: "test_ephemeral",
Defaults: cty.StringVal("current"),
},
"test_ephemeral_two": {
Mode: addrs.EphemeralResourceMode,
Type: "test_ephemeral_two",
Defaults: cty.StringVal("target"),
},
},
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
},
},
"ephemeral_collision_skipped": {
current: &MockData{
MockResources: map[string]*MockResource{},
MockDataSources: map[string]*MockResource{},
MockEphemeralResources: map[string]*MockResource{
"test_ephemeral": {
Mode: addrs.EphemeralResourceMode,
Type: "test_ephemeral",
Defaults: cty.StringVal("current"),
},
},
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
},
target: &MockData{
MockResources: map[string]*MockResource{},
MockDataSources: map[string]*MockResource{},
MockEphemeralResources: map[string]*MockResource{
"test_ephemeral": {
Mode: addrs.EphemeralResourceMode,
Type: "test_ephemeral",
Defaults: cty.StringVal("target"),
},
},
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
},
result: &MockData{
MockResources: map[string]*MockResource{},
MockDataSources: map[string]*MockResource{},
MockEphemeralResources: map[string]*MockResource{
"test_ephemeral": {
Mode: addrs.EphemeralResourceMode,
Type: "test_ephemeral",
Defaults: cty.StringVal("current"), // current wins when skipCollisions=true
},
},
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
},
},
}

for name, tc := range tcs {
Expand Down Expand Up @@ -538,6 +619,25 @@ func validateMockData(t *testing.T, actual, expected *MockData) {
}
}

// Validate mock ephemeral resources.

for key, actual := range actual.MockEphemeralResources {
expected, exists := expected.MockEphemeralResources[key]
if !exists {
t.Errorf("actual mock ephemeral resources contained %s but expected mock ephemeral resources did not", key)
continue
}

validateValues(t, key, actual.Defaults, expected.Defaults)
}

for key := range expected.MockEphemeralResources {
_, exists := actual.MockEphemeralResources[key]
if !exists {
t.Errorf("expected mock ephemeral resources contained %s but actual mock ephemeral resources did not", key)
}
}

// Validate the overrides.

for _, elem := range actual.Overrides.Elems {
Expand Down
78 changes: 34 additions & 44 deletions internal/providers/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,59 +362,49 @@ func (m *Mock) ReadDataSource(request ReadDataSourceRequest) ReadDataSourceRespo
return response
}

func (m *Mock) ValidateEphemeralResourceConfig(ValidateEphemeralResourceConfigRequest) ValidateEphemeralResourceConfigResponse {
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"No ephemeral resource types in mock providers",
"The provider mocking mechanism does not yet support ephemeral resource types.",
nil, // the topmost configuration object
))
return ValidateEphemeralResourceConfigResponse{
Diagnostics: diags,
}
func (m *Mock) ValidateEphemeralResourceConfig(request ValidateEphemeralResourceConfigRequest) ValidateEphemeralResourceConfigResponse {
// Delegate to the underlying provider so it can validate the configuration
// against the real schema, exactly as we do for resources and data sources.
return m.Provider.ValidateEphemeralResourceConfig(request)
}

func (m *Mock) OpenEphemeralResource(OpenEphemeralResourceRequest) OpenEphemeralResourceResponse {
// FIXME: Design some means to mock an ephemeral resource type.
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"No ephemeral resource types in mock providers",
"The provider mocking mechanism does not yet support ephemeral resource types.",
nil, // the topmost configuration object
))
return OpenEphemeralResourceResponse{
Diagnostics: diags,
func (m *Mock) OpenEphemeralResource(request OpenEphemeralResourceRequest) OpenEphemeralResourceResponse {
var response OpenEphemeralResourceResponse

schema := m.GetProviderSchema()
response.Diagnostics = response.Diagnostics.Append(schema.Diagnostics)
if schema.Diagnostics.HasErrors() {
return response
}

ephemeralSchema, exists := schema.EphemeralResourceTypes[request.TypeName]
if !exists {
// Should have been caught during validation.
panic(fmt.Errorf("failed to retrieve schema for ephemeral resource %s", request.TypeName))
}

mockedData := &mocking.MockedData{
Value: cty.NilVal, // If we have no mocked data we use cty.NilVal.
}
if mockedEphemeral, exists := m.Data.MockEphemeralResources[request.TypeName]; exists {
mockedData.Value = mockedEphemeral.Defaults
mockedData.Range = mockedEphemeral.DefaultsRange
}

value, diags := mocking.ComputedValuesForDataSource(request.Config, mockedData, ephemeralSchema.Body)
response.Diagnostics = response.Diagnostics.Append(diags)
response.Result = value
return response
}

func (m *Mock) RenewEphemeralResource(RenewEphemeralResourceRequest) RenewEphemeralResourceResponse {
// FIXME: Design some means to mock an ephemeral resource type.
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"No ephemeral resource types in mock providers",
"The provider mocking mechanism does not yet support ephemeral resource types.",
nil, // the topmost configuration object
))
return RenewEphemeralResourceResponse{
Diagnostics: diags,
}
// Mocked ephemeral resources never expire, so renewal is a no-op.
return RenewEphemeralResourceResponse{}
}

func (m *Mock) CloseEphemeralResource(CloseEphemeralResourceRequest) CloseEphemeralResourceResponse {
// FIXME: Design some means to mock an ephemeral resource type.
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"No ephemeral resource types in mock providers",
"The provider mocking mechanism does not yet support ephemeral resource types.",
nil, // the topmost configuration object
))
return CloseEphemeralResourceResponse{
Diagnostics: diags,
}
// Nothing to close for a mocked ephemeral resource.
return CloseEphemeralResourceResponse{}
}

func (m *Mock) CallFunction(request CallFunctionRequest) CallFunctionResponse {
Expand Down
Loading