// 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) } }