blob: 92ae6dcdf9bd08a7e777141bab4336ac53d032db [file]
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package rpcapi
import (
"bytes"
"errors"
"fmt"
"github.com/zclconf/go-cty/cty"
msgpack "github.com/zclconf/go-cty/cty/msgpack"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/policy"
"github.com/hashicorp/terraform/internal/policy/proto"
"github.com/hashicorp/terraform/internal/rpcapi/terraform1"
"github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackruntime"
"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func diagnosticsToProto(diags tfdiags.Diagnostics) []*terraform1.Diagnostic {
if len(diags) == 0 {
return nil
}
ret := make([]*terraform1.Diagnostic, len(diags))
for i, diag := range diags {
ret[i] = diagnosticToProto(diag)
}
return ret
}
func diagnosticToProto(diag tfdiags.Diagnostic) *terraform1.Diagnostic {
protoDiag := &terraform1.Diagnostic{}
switch diag.Severity() {
case tfdiags.Error:
protoDiag.Severity = terraform1.Diagnostic_ERROR
case tfdiags.Warning:
protoDiag.Severity = terraform1.Diagnostic_WARNING
default:
protoDiag.Severity = terraform1.Diagnostic_INVALID
}
desc := diag.Description()
protoDiag.Summary = desc.Summary
protoDiag.Detail = desc.Detail
srcRngs := diag.Source()
if srcRngs.Subject != nil {
protoDiag.Subject = sourceRangeToProto(*srcRngs.Subject)
}
if srcRngs.Context != nil {
protoDiag.Context = sourceRangeToProto(*srcRngs.Context)
}
return protoDiag
}
func sourceRangeToProto(rng tfdiags.SourceRange) *terraform1.SourceRange {
return &terraform1.SourceRange{
// RPC API operations use source address syntax for "filename" by
// convention, because the physical filesystem layout is an
// implementation detail.
SourceAddr: rng.Filename,
Start: sourcePosToProto(rng.Start),
End: sourcePosToProto(rng.End),
}
}
func sourceRangeFromProto(protoRng *terraform1.SourceRange) tfdiags.SourceRange {
return tfdiags.SourceRange{
Filename: protoRng.SourceAddr,
Start: sourcePosFromProto(protoRng.Start),
End: sourcePosFromProto(protoRng.End),
}
}
func sourceRangeFromHCL(rng hcl.Range) tfdiags.SourceRange {
return tfdiags.SourceRange{
Filename: rng.Filename,
Start: tfdiags.SourcePos{
Line: rng.Start.Line,
Column: rng.Start.Column,
Byte: rng.Start.Byte,
},
End: tfdiags.SourcePos{
Line: rng.End.Line,
Column: rng.End.Column,
Byte: rng.End.Byte,
},
}
}
func sourcePosToProto(pos tfdiags.SourcePos) *terraform1.SourcePos {
return &terraform1.SourcePos{
Byte: int64(pos.Byte),
Line: int64(pos.Line),
Column: int64(pos.Column),
}
}
func sourcePosFromProto(protoPos *terraform1.SourcePos) tfdiags.SourcePos {
return tfdiags.SourcePos{
Byte: int(protoPos.Byte),
Line: int(protoPos.Line),
Column: int(protoPos.Column),
}
}
func dynamicTypedValueFromProto(protoVal *stacks.DynamicValue) (cty.Value, error) {
if len(protoVal.Msgpack) == 0 {
return cty.DynamicVal, fmt.Errorf("uses unsupported serialization format (only MessagePack is supported)")
}
v, err := msgpack.Unmarshal(protoVal.Msgpack, cty.DynamicPseudoType)
if err != nil {
return cty.DynamicVal, fmt.Errorf("invalid serialization: %w", err)
}
// FIXME: Incredibly imprecise handling of sensitive values. We should
// actually decode the attribute paths and mark individual leaf attributes
// that are sensitive, but for now we'll just mark the whole thing as
// sensitive if any part of it is sensitive.
if len(protoVal.Sensitive) != 0 {
v = v.Mark(marks.Sensitive)
}
return v, nil
}
func externalInputValuesFromProto(protoVals map[string]*stacks.DynamicValueWithSource) (map[stackaddrs.InputVariable]stackruntime.ExternalInputValue, error) {
if len(protoVals) == 0 {
return nil, nil
}
var err error
ret := make(map[stackaddrs.InputVariable]stackruntime.ExternalInputValue, len(protoVals))
for name, protoVal := range protoVals {
v, moreErr := externalInputValueFromProto(protoVal)
if moreErr != nil {
err = errors.Join(err, fmt.Errorf("%s: %w", name, moreErr))
}
ret[stackaddrs.InputVariable{Name: name}] = v
}
return ret, err
}
func externalInputValueFromProto(protoVal *stacks.DynamicValueWithSource) (stackruntime.ExternalInputValue, error) {
v, err := dynamicTypedValueFromProto(protoVal.Value)
if err != nil {
return stackruntime.ExternalInputValue{}, nil
}
rng := sourceRangeFromProto(protoVal.SourceRange)
return stackruntime.ExternalInputValue{
Value: v,
DefRange: rng,
}, nil
}
func componentInstancePolicyEvaluationProto(componentInstanceAddr stackaddrs.AbsComponentInstance, policyResults map[string]policy.EvaluationResponse) *stacks.ComponentInstancePolicyEvaluation {
results := make([]*stacks.PolicyResult, 0)
infos := make([]*stacks.PolicyInfo, 0)
policyDiags := make([]*stacks.PolicyDiagnostic, 0)
for addr, result := range policyResults {
results = append(results, policyResultsToProto(addr, result.Policies)...)
infos = append(infos, policyInfosToProto(addr, result.Enforcements)...)
policyDiags = append(policyDiags, policyDiagsToProto(addr, result.Diagnostics)...)
}
return &stacks.ComponentInstancePolicyEvaluation{
Addr: &stacks.ComponentInstanceInStackAddr{
ComponentAddr: stackaddrs.ConfigComponentForAbsInstance(componentInstanceAddr).String(),
ComponentInstanceAddr: componentInstanceAddr.String(),
},
Results: results,
Infos: infos,
Diagnostics: policyDiags,
}
}
func providerInstancePolicyEvaluationProto(result *hooks.ProviderInstancePolicyResults) *stacks.ProviderInstancePolicyEvaluation {
resp := result.Result
results := policyResultsToProto(result.ProviderAddr, resp.Policies)
infos := policyInfosToProto(result.ProviderAddr, resp.Enforcements)
policyDiags := policyDiagsToProto(result.ProviderAddr, resp.Diagnostics)
return &stacks.ProviderInstancePolicyEvaluation{
Addr: &stacks.ProviderInstanceInStackAddr{
ProviderAddr: stackaddrs.ConfigProviderConfigForAbsInstance(result.Addr).String(),
ProviderInstanceAddr: result.Addr.String(),
},
Results: results,
Infos: infos,
Diagnostics: policyDiags,
}
}
func policyEvaluateResultToProto(result policy.EvaluateResult) stacks.EvaluateResult {
switch result {
case policy.InvalidResult:
return stacks.EvaluateResult_INVALID_EVALUATE_RESULT
case policy.UnknownResult:
return stacks.EvaluateResult_UNKNOWN_EVALUATE_RESULT
case policy.PolicyErrorResult:
return stacks.EvaluateResult_ERROR_EVALUATE_RESULT
case policy.AllowResult:
return stacks.EvaluateResult_ALLOW_EVALUATE_RESULT
case policy.DenyResult:
return stacks.EvaluateResult_DENY_EVALUATE_RESULT
case policy.SetupErrorResult:
return stacks.EvaluateResult_SETUP_ERROR_EVALUATE_RESULT
default:
// should be exhaustive
panic(fmt.Errorf("unhandled policy.EvaluateResult type: %T", result))
}
}
func policyResultsToProto(addr string, policies []*policy.Policy) []*stacks.PolicyResult {
protoPolicyResults := make([]*stacks.PolicyResult, len(policies))
for i, policy := range policies {
result := stacks.PolicyResult{
TargetAddress: addr,
PolicyMetadata: policyMetadataToProto(policy, nil),
Result: policyEvaluateResultToProto(policy.Result),
}
protoPolicyResults[i] = &result
}
return protoPolicyResults
}
func policyMetadataToProto(policyObj *policy.Policy, enforceIndex *int32) *stacks.PolicyMetaData {
if policyObj == nil {
return nil
}
metadata := &stacks.PolicyMetaData{
PolicySetName: policyObj.PolicySetName,
PolicyName: policyObj.Address,
FileName: policyObj.Filename,
EnforcementLevel: policyObj.EnforcementLevel,
}
if enforceIndex != nil {
metadata.EnforceIndex = *enforceIndex
}
return metadata
}
func policyInfosToProto(addr string, enforcements []policy.EnforcementResult) []*stacks.PolicyInfo {
protoPolicyInfos := make([]*stacks.PolicyInfo, 0)
for _, enforcement := range enforcements {
if enforcement.Message == "" {
continue
}
protoPolicyMetadata := policyMetadataToProto(enforcement.Policy, &enforcement.BlockIndex)
var protoPolicySnippet *stacks.PolicySnippet
if snippet := enforcement.Snippet; snippet != nil {
protoPolicySnippet = &stacks.PolicySnippet{
Code: snippet.Code,
StartLine: snippet.StartLine,
HighlightStartOffset: snippet.HighlightStartOffset,
HighlightEndOffset: snippet.HighlightEndOffset,
}
if snippet.Context != nil {
protoPolicySnippet.Context = *snippet.Context
}
}
var protoPolicyRange *terraform1.SourceRange
if enforcement.Range != nil {
rng := sourceRangeFromHCL(*enforcement.Range)
protoPolicyRange = &terraform1.SourceRange{
SourceAddr: enforcement.Range.Filename,
Start: sourcePosToProto(rng.Start),
End: sourcePosToProto(rng.End),
}
}
protoPolicyInfos = append(protoPolicyInfos, &stacks.PolicyInfo{
TargetAddress: addr,
Result: policyEvaluateResultToProto(enforcement.Result),
Message: enforcement.Message,
PolicySnippet: protoPolicySnippet,
PolicyMetadata: protoPolicyMetadata,
PolicyRange: protoPolicyRange,
})
}
return protoPolicyInfos
}
func policyDiagsToProto(addr string, policyDiags policy.Diagnostics) []*stacks.PolicyDiagnostic {
protoPolicyDiags := make([]*stacks.PolicyDiagnostic, len(policyDiags))
for i, diag := range policyDiags {
desc := diag.Description()
extra := tfdiags.ExtraInfo[*policy.PolicyExtra](diag)
policyDiag := stacks.PolicyDiagnostic{
TargetAddress: addr,
Diagnostic: &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: desc.Summary,
Detail: desc.Detail,
},
}
if extra != nil {
if extra.Severity == hcl.DiagWarning {
policyDiag.Diagnostic.Severity = terraform1.Diagnostic_WARNING
}
policyDiag.Result = policyEvaluateResultToProto(extra.Result)
policyDiag.PolicyMetadata = policyMetadataToProto(&extra.Policy, extra.EnforceIndex)
if snippet := extra.Snippet; snippet != nil {
policyDiag.PolicySnippet = &stacks.PolicySnippet{
Code: snippet.Code,
StartLine: snippet.StartLine,
HighlightStartOffset: snippet.HighlightStartOffset,
HighlightEndOffset: snippet.HighlightEndOffset,
}
if snippet.Context != nil {
policyDiag.PolicySnippet.Context = *snippet.Context
}
}
if rng := extra.Range; rng != nil && rng.Subject != nil {
policyDiag.PolicyRange = &terraform1.SourceRange{
SourceAddr: rng.Subject.Filename,
}
if start := rng.Subject.Start; start != nil {
policyDiag.PolicyRange.Start = &terraform1.SourcePos{
Line: start.Line,
Column: start.Column,
Byte: start.Byte,
}
}
if end := rng.Subject.End; end != nil {
policyDiag.PolicyRange.End = &terraform1.SourcePos{
Line: end.Line,
Column: end.Column,
Byte: end.Byte,
}
}
}
policyDiag.ExpressionValues = policyExpressionValuesToProto(extra.ExpressionValues)
}
if src := diag.Source(); src.Subject != nil {
policyDiag.Diagnostic.Subject = sourceRangeToProto(*src.Subject)
}
if src := diag.Source(); src.Context != nil {
policyDiag.Diagnostic.Context = sourceRangeToProto(*src.Context)
}
protoPolicyDiags[i] = &policyDiag
}
return protoPolicyDiags
}
func policyExpressionValuesToProto(policyExpressionValues []*proto.ExpressionValue) []*stacks.ExpressionValue {
if len(policyExpressionValues) == 0 {
return nil
}
expressionValues := make([]*stacks.ExpressionValue, 0, len(policyExpressionValues))
seen := make(map[string]struct{}, len(policyExpressionValues))
for _, val := range policyExpressionValues {
path, err := val.Traversal.ToCtyPath()
if err != nil {
continue // then we can't display this value
}
exprValue := &stacks.ExpressionValue{
Traversal: stacks.NewAttributePath(path),
}
strPath := ctyPathStr(path)
if _, exists := seen[strPath]; exists {
continue
}
seen[strPath] = struct{}{}
exprValue.Value = val.Value
expressionValues = append(expressionValues, exprValue)
}
return expressionValues
}
func ctyPathStr(path cty.Path) string {
// This is a specialized subset of traversal rendering tailored to
// producing a string that can be used to detect duplicate cty paths.
// It is not comprehensive nor intended to be used for other purposes.
var buf bytes.Buffer
first := true
for _, step := range path {
switch tStep := step.(type) {
case cty.GetAttrStep:
if !first {
buf.WriteByte('.')
}
buf.WriteString(tStep.Name)
case cty.IndexStep:
buf.WriteByte('[')
if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {
buf.WriteString(tfdiags.CompactValueStr(tStep.Key))
} else {
// We'll just use a placeholder for more complex values,
// since otherwise our result could grow ridiculously long.
buf.WriteString("...")
}
buf.WriteByte(']')
}
first = false
}
return buf.String()
}