Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New terraform_data managed resource to replace null_resource #31757

Merged
merged 3 commits into from
Dec 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 14 additions & 20 deletions internal/builtin/providers/terraform/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,7 @@ import (
)

// Provider is an implementation of providers.Interface
type Provider struct {
// Provider is the schema for the provider itself.
Schema providers.Schema

// DataSources maps the data source name to that data source's schema.
DataSources map[string]providers.Schema
}
type Provider struct{}

// NewProvider returns a new terraform provider
func NewProvider() providers.Interface {
Expand All @@ -27,6 +21,9 @@ func (p *Provider) GetProviderSchema() providers.GetProviderSchemaResponse {
DataSources: map[string]providers.Schema{
"terraform_remote_state": dataSourceRemoteStateGetSchema(),
},
ResourceTypes: map[string]providers.Schema{
"terraform_data": dataStoreResourceSchema(),
},
}
}

Expand Down Expand Up @@ -99,26 +96,26 @@ func (p *Provider) Stop() error {
// instance state whose schema version is less than the one reported by the
// currently-used version of the corresponding provider, and the upgraded
// result is used for any further processing.
func (p *Provider) UpgradeResourceState(providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
panic("unimplemented - terraform_remote_state has no resources")
func (p *Provider) UpgradeResourceState(req providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
return upgradeDataStoreResourceState(req)
}

// ReadResource refreshes a resource and returns its current state.
func (p *Provider) ReadResource(providers.ReadResourceRequest) providers.ReadResourceResponse {
panic("unimplemented - terraform_remote_state has no resources")
func (p *Provider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse {
return readDataStoreResourceState(req)
}

// PlanResourceChange takes the current state and proposed state of a
// resource, and returns the planned final state.
func (p *Provider) PlanResourceChange(providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
panic("unimplemented - terraform_remote_state has no resources")
func (p *Provider) PlanResourceChange(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return planDataStoreResourceChange(req)
}

// ApplyResourceChange takes the planned state for a resource, which may
// yet contain unknown computed values, and applies the changes returning
// the final state.
func (p *Provider) ApplyResourceChange(providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
panic("unimplemented - terraform_remote_state has no resources")
func (p *Provider) ApplyResourceChange(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
return applyDataStoreResourceChange(req)
}

// ImportResourceState requests that the given resource be imported.
Expand All @@ -127,11 +124,8 @@ func (p *Provider) ImportResourceState(providers.ImportResourceStateRequest) pro
}

// ValidateResourceConfig is used to to validate the resource configuration values.
func (p *Provider) ValidateResourceConfig(providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
// At this moment there is nothing to configure for the terraform provider,
// so we will happily return without taking any action
var res providers.ValidateResourceConfigResponse
return res
func (p *Provider) ValidateResourceConfig(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
return validateDataStoreResourceConfig(req)
}

// Close is a noop for this provider, since it's run in-process.
Expand Down
146 changes: 146 additions & 0 deletions internal/builtin/providers/terraform/resource_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package terraform

import (
"fmt"

"github.com/hashicorp/go-uuid"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
)

func dataStoreResourceSchema() providers.Schema {
return providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"input": {Type: cty.DynamicPseudoType, Optional: true},
"output": {Type: cty.DynamicPseudoType, Computed: true},
"triggers_replace": {Type: cty.DynamicPseudoType, Optional: true},
"id": {Type: cty.String, Computed: true},
},
},
}
}

func validateDataStoreResourceConfig(req providers.ValidateResourceConfigRequest) (resp providers.ValidateResourceConfigResponse) {
if req.Config.IsNull() {
return resp
}

// Core does not currently validate computed values are not set in the
// configuration.
for _, attr := range []string{"id", "output"} {
if !req.Config.GetAttr(attr).IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf(`%q attribute is read-only`, attr))
}
}
return resp
}

func upgradeDataStoreResourceState(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) {
ty := dataStoreResourceSchema().Block.ImpliedType()
val, err := ctyjson.Unmarshal(req.RawStateJSON, ty)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}

resp.UpgradedState = val
return resp
}

func readDataStoreResourceState(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
resp.NewState = req.PriorState
return resp
}

func planDataStoreResourceChange(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
if req.ProposedNewState.IsNull() {
// destroy op
resp.PlannedState = req.ProposedNewState
return resp
}

planned := req.ProposedNewState.AsValueMap()

input := req.ProposedNewState.GetAttr("input")
trigger := req.ProposedNewState.GetAttr("triggers_replace")

switch {
case req.PriorState.IsNull():
// Create
// Set the id value to unknown.
planned["id"] = cty.UnknownVal(cty.String)

// Only compute a new output if input has a non-null value.
if !input.IsNull() {
planned["output"] = cty.UnknownVal(input.Type())
}

resp.PlannedState = cty.ObjectVal(planned)
return resp

case !req.PriorState.GetAttr("triggers_replace").RawEquals(trigger):
// trigger changed, so we need to replace the entire instance
resp.RequiresReplace = append(resp.RequiresReplace, cty.GetAttrPath("triggers_replace"))
planned["id"] = cty.UnknownVal(cty.String)

// We need to check the input for the replacement instance to compute a
// new output.
if input.IsNull() {
planned["output"] = cty.NullVal(cty.DynamicPseudoType)
} else {
planned["output"] = cty.UnknownVal(input.Type())
}

case !req.PriorState.GetAttr("input").RawEquals(input):
// only input changed, so we only need to re-compute output
planned["output"] = cty.UnknownVal(input.Type())
}

resp.PlannedState = cty.ObjectVal(planned)
return resp
}

var testUUIDHook func() string

func applyDataStoreResourceChange(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
if req.PlannedState.IsNull() {
resp.NewState = req.PlannedState
return resp
}

newState := req.PlannedState.AsValueMap()

if !req.PlannedState.GetAttr("output").IsKnown() {
newState["output"] = req.PlannedState.GetAttr("input")
}

if !req.PlannedState.GetAttr("id").IsKnown() {
idString, err := uuid.GenerateUUID()
// Terraform would probably never get this far without a good random
// source, but catch the error anyway.
if err != nil {
diag := tfdiags.AttributeValue(
tfdiags.Error,
"Error generating id",
err.Error(),
cty.GetAttrPath("id"),
)

resp.Diagnostics = resp.Diagnostics.Append(diag)
}

if testUUIDHook != nil {
idString = testUUIDHook()
}

newState["id"] = cty.StringVal(idString)
}

resp.NewState = cty.ObjectVal(newState)

return resp
}