Files
opentf/internal/provider-simple-v6/provider.go
Andrei Ciobanu 1bab9aff46 Ephemeral todos handling (#3177)
Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
2025-09-10 07:45:23 -04:00

260 lines
8.4 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// simple provider a minimal provider implementation for testing
package simple
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/providers"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
type simple struct {
schema providers.GetProviderSchemaResponse
}
func Provider() providers.Interface {
simpleResource := providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Computed: true,
Type: cty.String,
},
"value": {
Optional: true,
Type: cty.String,
},
},
},
}
// Only managed resource should have write-only arguments.
withWriteOnlyBlocks := func(s providers.Schema) providers.Schema {
b := *s.Block
b.Attributes["value_wo"] = &configschema.Attribute{
Optional: true,
Type: cty.String,
WriteOnly: true,
}
b.BlockTypes = map[string]*configschema.NestedBlock{
"nested_block": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"nested_block_attr": {
Type: cty.String,
Optional: true,
WriteOnly: true,
},
},
},
},
}
return providers.Schema{Block: &b}
}
return simple{
schema: providers.GetProviderSchemaResponse{
Provider: providers.Schema{
// The "i_depend_on" field is just a simple configuration attribute of the provider
// to allow creation of dependencies between a resources from a previously
// initialized provider and this provider.
// The "i_depend_on" field is having no functionality behind, in the provider context,
// but it's just a way for the "provider" block to create depedencies
// to other blocks.
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"i_depend_on": {
Type: cty.String,
Description: "Non-functional configuration attribute of the provider. This is meant to be used only to create depedencies of other resources to the provider block",
Optional: true,
},
},
},
},
ResourceTypes: map[string]providers.Schema{
"simple_resource": withWriteOnlyBlocks(simpleResource),
},
DataSources: map[string]providers.Schema{
"simple_resource": simpleResource,
},
EphemeralResources: map[string]providers.Schema{
"simple_resource": simpleResource,
},
ServerCapabilities: providers.ServerCapabilities{
PlanDestroy: true,
},
},
}
}
func (s simple) GetProviderSchema(_ context.Context) providers.GetProviderSchemaResponse {
return s.schema
}
func (s simple) ValidateProviderConfig(_ context.Context, req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) {
return resp
}
func (s simple) ValidateResourceConfig(_ context.Context, req providers.ValidateResourceConfigRequest) (resp providers.ValidateResourceConfigResponse) {
return resp
}
func (s simple) ValidateDataResourceConfig(_ context.Context, req providers.ValidateDataResourceConfigRequest) (resp providers.ValidateDataResourceConfigResponse) {
return resp
}
func (s simple) ValidateEphemeralConfig(context.Context, providers.ValidateEphemeralConfigRequest) (resp providers.ValidateEphemeralConfigResponse) {
return resp
}
func (s simple) MoveResourceState(_ context.Context, req providers.MoveResourceStateRequest) providers.MoveResourceStateResponse {
var resp providers.MoveResourceStateResponse
val, err := ctyjson.Unmarshal(req.SourceStateJSON, s.schema.ResourceTypes["simple_resource"].Block.ImpliedType())
resp.Diagnostics = resp.Diagnostics.Append(err)
if err != nil {
return resp
}
resp.TargetState = val
resp.TargetPrivate = req.SourcePrivate
return resp
}
func (s simple) UpgradeResourceState(_ context.Context, req providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
var resp providers.UpgradeResourceStateResponse
ty := s.schema.ResourceTypes[req.TypeName].Block.ImpliedType()
val, err := ctyjson.Unmarshal(req.RawStateJSON, ty)
resp.Diagnostics = resp.Diagnostics.Append(err)
resp.UpgradedState = val
return resp
}
func (s simple) ConfigureProvider(context.Context, providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
return resp
}
func (s simple) Stop(_ context.Context) error {
return nil
}
func (s simple) ReadResource(_ context.Context, req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
// just return the same state we received
resp.NewState = req.PriorState
return resp
}
func (s simple) PlanResourceChange(_ context.Context, req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
if req.ProposedNewState.IsNull() {
// destroy op
resp.PlannedState = req.ProposedNewState
// signal that this resource was properly planned for destruction,
// verifying that the schema capabilities with PlanDestroy took effect.
resp.PlannedPrivate = []byte("destroy planned")
return resp
}
m := req.ProposedNewState.AsValueMap()
_, ok := m["id"]
if !ok {
m["id"] = cty.UnknownVal(cty.String)
}
// Simulate what the terraform-plugin-go should do. Nullify the write-only attributes.
m["value_wo"] = cty.NullVal(cty.String)
resp.PlannedState = cty.ObjectVal(m)
return resp
}
func (s simple) ApplyResourceChange(_ context.Context, req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
if req.PlannedState.IsNull() {
// make sure this was transferred from the plan action
if string(req.PlannedPrivate) != "destroy planned" {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("resource not planned for destroy, private data %q", req.PlannedPrivate))
}
resp.NewState = req.PlannedState
return resp
}
m := req.PlannedState.AsValueMap()
_, ok := m["id"]
if !ok {
m["id"] = cty.StringVal(time.Now().String())
}
waitIfRequested(req.Config.AsValueMap())
// Simulate what the terraform-plugin-go should do. Nullify the write-only attributes.
m["value_wo"] = cty.NullVal(cty.String)
resp.NewState = cty.ObjectVal(m)
return resp
}
func (s simple) ImportResourceState(context.Context, providers.ImportResourceStateRequest) (resp providers.ImportResourceStateResponse) {
resp.Diagnostics = resp.Diagnostics.Append(errors.New("unsupported"))
return resp
}
func (s simple) ReadDataSource(_ context.Context, req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) {
m := req.Config.AsValueMap()
m["id"] = cty.StringVal("static_id")
resp.State = cty.ObjectVal(m)
return resp
}
func (s simple) OpenEphemeralResource(_ context.Context, req providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) {
m := req.Config.AsValueMap()
m["id"] = cty.StringVal("static-ephemeral-id")
if v, ok := m["value"]; ok && !v.IsNull() && strings.Contains(v.AsString(), "with-renew") {
t := time.Now().Add(200 * time.Millisecond)
resp.RenewAt = &t
}
resp.Result = cty.ObjectVal(m)
resp.Private = []byte("static private data")
return resp
}
func (s simple) RenewEphemeralResource(_ context.Context, req providers.RenewEphemeralResourceRequest) (resp providers.RenewEphemeralResourceResponse) {
resp.Private = []byte(fmt.Sprintf("%s - renew", req.Private))
t := time.Now().Add(200 * time.Millisecond)
resp.RenewAt = &t
return resp
}
func (s simple) CloseEphemeralResource(context.Context, providers.CloseEphemeralResourceRequest) (resp providers.CloseEphemeralResourceResponse) {
return resp
}
func (s simple) GetFunctions(context.Context) providers.GetFunctionsResponse {
panic("Not Implemented")
}
func (s simple) CallFunction(_ context.Context, r providers.CallFunctionRequest) providers.CallFunctionResponse {
panic("Not Implemented")
}
func (s simple) Close(_ context.Context) error {
return nil
}
func waitIfRequested(m map[string]cty.Value) {
// This is a special case that can be used together with ephemeral resources to be able to test the renewal process.
// When the "value_wo" attribute of the resource is containing "with-renew" it will return later to allow
// the ephemeral resource to call renew at least once. Check also OpenEphemeralResource.
if v, ok := m["value_wo"]; ok && !v.IsNull() && strings.Contains(v.AsString(), "with-renew") {
<-time.After(time.Second)
}
}