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

Ensure WriteOnly attributes are rejected as non-ephemeral output values #36171

Closed
wants to merge 15 commits into from
Closed
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
4 changes: 4 additions & 0 deletions docs/plugin-protocol/tfplugin5.8.proto
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ message ClientCapabilities {
// The deferral_allowed capability signals that the client is able to
// handle deferred responses from the provider.
bool deferral_allowed = 1;

// The write_only_attributes_allowed capability signals that the client
// is able to handle write_only attributes for managed resources.
bool write_only_attributes_allowed = 2;
}

message Function {
Expand Down
4 changes: 4 additions & 0 deletions docs/plugin-protocol/tfplugin6.8.proto
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ message ClientCapabilities {
// The deferral_allowed capability signals that the client is able to
// handle deferred responses from the provider.
bool deferral_allowed = 1;

// The write_only_attributes_allowed capability signals that the client
// is able to handle write_only attributes for managed resources.
bool write_only_attributes_allowed = 2;
}

// Deferred is a message that indicates that change is deferred for a reason.
Expand Down
2 changes: 1 addition & 1 deletion internal/backend/local/backend_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (b *Local) opApply(
// actions but that any it does include will be properly-formed.
// plan.Errored will be true in this case, which our plan
// renderer can rely on to tailor its messaging.
if plan != nil && (len(plan.Changes.Resources) != 0 || len(plan.Changes.Outputs) != 0) {
if plan != nil && plan.Changes != nil && (len(plan.Changes.Resources) != 0 || len(plan.Changes.Outputs) != 0) {
op.View.Plan(plan, schemas)
}
op.ReportResult(runningOp, diags)
Expand Down
69 changes: 69 additions & 0 deletions internal/builtin/providers/terraform/example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package terraform

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

func exampleResourceSchema() providers.Schema {
return providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"wo_attr": {
Type: cty.String,
Optional: true,
WriteOnly: true,
},
"foo": {
Type: cty.String,
Optional: true,
},
},
},
}
}

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

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

planned := req.ProposedNewState.AsValueMap()
planned["wo_attr"] = cty.NullVal(cty.String)
resp.PlannedState = cty.ObjectVal(planned)
return resp
}

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

newState := req.PlannedState.AsValueMap()
newState["wo_attr"] = cty.NullVal(cty.String)
resp.NewState = cty.ObjectVal(newState)

return resp
}

func upgradeExample(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) {
ty := exampleResourceSchema().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
}
43 changes: 38 additions & 5 deletions internal/builtin/providers/terraform/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ func (p *Provider) GetProviderSchema() providers.GetProviderSchemaResponse {
"terraform_remote_state": dataSourceRemoteStateGetSchema(),
},
ResourceTypes: map[string]providers.Schema{
"terraform_data": dataStoreResourceSchema(),
"terraform_data": dataStoreResourceSchema(),
"terraform_example": exampleResourceSchema(),
},
EphemeralResourceTypes: map[string]providers.Schema{},
Functions: map[string]providers.FunctionDecl{
Expand Down Expand Up @@ -147,25 +148,50 @@ func (p *Provider) Stop() error {
// currently-used version of the corresponding provider, and the upgraded
// result is used for any further processing.
func (p *Provider) UpgradeResourceState(req providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
return upgradeDataStoreResourceState(req)
switch req.TypeName {
case "terraform_data":
return upgradeDataStoreResourceState(req)
case "terraform_example":
return upgradeExample(req)
}
panic("unimplemented: cannot upgrade resource type " + req.TypeName)
}

// ReadResource refreshes a resource and returns its current state.
func (p *Provider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse {
return readDataStoreResourceState(req)
switch req.TypeName {
case "terraform_data":
return readDataStoreResourceState(req)
case "terraform_example":
return readExample(req)
}
panic("unimplemented: cannot read resource type " + req.TypeName)
}

// PlanResourceChange takes the current state and proposed state of a
// resource, and returns the planned final state.
func (p *Provider) PlanResourceChange(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return planDataStoreResourceChange(req)
switch req.TypeName {
case "terraform_data":
return planDataStoreResourceChange(req)
case "terraform_example":
return planExample(req)
}
panic("unimplemented: cannot plan resource type " + req.TypeName)
}

// 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(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
return applyDataStoreResourceChange(req)
switch req.TypeName {
case "terraform_data":
return applyDataStoreResourceChange(req)
case "terraform_example":
return applyExample(req)
}

panic("unimplemented: cannot apply resource type " + req.TypeName)
}

// ImportResourceState requests that the given resource be imported.
Expand Down Expand Up @@ -193,6 +219,13 @@ func (p *Provider) MoveResourceState(req providers.MoveResourceStateRequest) pro

// ValidateResourceConfig is used to to validate the resource configuration values.
func (p *Provider) ValidateResourceConfig(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
var res providers.ValidateResourceConfigResponse
// This should not happen
if req.TypeName != "terraform_data" {
res.Diagnostics.Append(fmt.Errorf("Error: unsupported resource %s", req.TypeName))
return res
}

return validateDataStoreResourceConfig(req)
}

Expand Down
1 change: 1 addition & 0 deletions internal/builtin/providers/terraform/resource_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func validateDataStoreResourceConfig(req providers.ValidateResourceConfigRequest
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf(`%q attribute is read-only`, attr))
}
}

return resp
}

Expand Down
38 changes: 38 additions & 0 deletions internal/configs/configschema/implied_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ func (b *Block) ContainsSensitive() bool {
return false
}

// ContainsWriteOnly returns true if any of the attributes of the receiving
// block or any of its descendant blocks are marked as WriteOnly.
//
// Blocks themselves cannot be WriteOnly as a whole -- sensitivity is a
// per-attribute idea -- but sometimes we want to include a whole object
// decoded from a block in some UI output, and that is safe to do only if
// none of the contained attributes are WriteOnly.
func (b *Block) ContainsWriteOnly() bool {
for _, attrS := range b.Attributes {
if attrS.WriteOnly {
return true
}
if attrS.NestedType != nil && attrS.NestedType.ContainsWriteOnly() {
return true
}
}
for _, blockS := range b.BlockTypes {
if blockS.ContainsWriteOnly() {
return true
}
}
return false
}

// ImpliedType returns the cty.Type that would result from decoding a Block's
// ImpliedType and getting the resulting AttributeType.
//
Expand Down Expand Up @@ -134,3 +158,17 @@ func (o *Object) ContainsSensitive() bool {
}
return false
}

// ContainsWriteOnly returns true if any of the attributes of the receiving
// Object are marked as WriteOnly.
func (o *Object) ContainsWriteOnly() bool {
for _, attrS := range o.Attributes {
if attrS.WriteOnly {
return true
}
if attrS.NestedType != nil && attrS.NestedType.ContainsWriteOnly() {
return true
}
}
return false
}
123 changes: 123 additions & 0 deletions internal/configs/configschema/marks.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,72 @@ func (b *Block) SensitivePaths(val cty.Value, basePath cty.Path) []cty.Path {
return ret
}

// WriteOnlyPaths returns a set of paths into the given value that should
// be marked as WriteOnly based on the static declarations in the schema.
func (b *Block) WriteOnlyPaths(val cty.Value, basePath cty.Path) []cty.Path {
var ret []cty.Path

// We can mark attributes as WriteOnly even if the value is null
for name, attrS := range b.Attributes {
if attrS.WriteOnly {
attrPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name})
ret = append(ret, attrPath)
}
}

// If the value is null, no other marks are possible
if val.IsNull() {
return ret
}

// Extract marks for nested attribute type values
for name, attrS := range b.Attributes {
// If the attribute has no nested type, or the nested type doesn't
// contain any write-only attributes, skip inspecting it
if attrS.NestedType == nil || !attrS.NestedType.ContainsWriteOnly() {
continue
}

// Create a copy of the path, with this step added, to add to our PathValueMarks slice
attrPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name})
ret = append(ret, attrS.NestedType.WriteOnlyPaths(val.GetAttr(name), attrPath)...)
}

// Extract marks for nested blocks
for name, blockS := range b.BlockTypes {
// If our block doesn't contain any WriteOnly attributes, skip inspecting it
if !blockS.Block.ContainsWriteOnly() {
continue
}

blockV := val.GetAttr(name)
if blockV.IsNull() || !blockV.IsKnown() {
continue
}

// Create a copy of the path, with this step added, to add to our PathValueMarks slice
blockPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name})

switch blockS.Nesting {
case NestingSingle, NestingGroup:
ret = append(ret, blockS.Block.WriteOnlyPaths(blockV, blockPath)...)
case NestingList, NestingMap, NestingSet:
blockV, _ = blockV.Unmark() // peel off one level of marking so we can iterate
for it := blockV.ElementIterator(); it.Next(); {
idx, blockEV := it.Element()
// Create a copy of the path, with this block instance's index
// step added, to add to our PathValueMarks slice
blockInstancePath := copyAndExtendPath(blockPath, cty.IndexStep{Key: idx})
morePaths := blockS.Block.WriteOnlyPaths(blockEV, blockInstancePath)
ret = append(ret, morePaths...)
}
default:
panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
}
}
return ret
}

// SensitivePaths returns a set of paths into the given value that should be
// marked as sensitive based on the static declarations in the schema.
func (o *Object) SensitivePaths(val cty.Value, basePath cty.Path) []cty.Path {
Expand Down Expand Up @@ -140,3 +206,60 @@ func (o *Object) SensitivePaths(val cty.Value, basePath cty.Path) []cty.Path {
}
return ret
}

// WriteOnlyPaths returns a set of paths into the given value that should be
// marked as WriteOnly based on the static declarations in the schema.
func (o *Object) WriteOnlyPaths(val cty.Value, basePath cty.Path) []cty.Path {
var ret []cty.Path

if val.IsNull() || !val.IsKnown() {
return ret
}

for name, attrS := range o.Attributes {
// Skip attributes which can never produce WriteOnly path value marks
if !attrS.WriteOnly && (attrS.NestedType == nil || !attrS.NestedType.ContainsWriteOnly()) {
continue
}

switch o.Nesting {
case NestingSingle, NestingGroup:
// Create a path to this attribute
attrPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name})

if attrS.WriteOnly {
// If the entire attribute is WriteOnly, mark it so
ret = append(ret, attrPath)
} else {
// The attribute has a nested type which contains WriteOnly
// attributes, so recurse
ret = append(ret, attrS.NestedType.WriteOnlyPaths(val.GetAttr(name), attrPath)...)
}
case NestingList, NestingMap, NestingSet:
// For nested attribute types which have a non-single nesting mode,
// we add path value marks for each element of the collection
val, _ = val.Unmark() // peel off one level of marking so we can iterate
for it := val.ElementIterator(); it.Next(); {
idx, attrEV := it.Element()
attrV := attrEV.GetAttr(name)

// Create a path to this element of the attribute's collection. Note
// that the path is extended in opposite order to the iteration order
// of the loops: index into the collection, then the contained
// attribute name. This is because we have one type
// representing multiple collection elements.
attrPath := copyAndExtendPath(basePath, cty.IndexStep{Key: idx}, cty.GetAttrStep{Name: name})

if attrS.WriteOnly {
// If the entire attribute is WriteOnly, mark it so
ret = append(ret, attrPath)
} else {
ret = append(ret, attrS.NestedType.WriteOnlyPaths(attrV, attrPath)...)
}
}
default:
panic(fmt.Sprintf("unsupported nesting mode %s", attrS.NestedType.Nesting))
}
}
return ret
}
Loading
Loading