diff --git a/internal/backend/remote-state/azure/go.sum b/internal/backend/remote-state/azure/go.sum index 5152575d0b4a..5f8d356fb909 100644 --- a/internal/backend/remote-state/azure/go.sum +++ b/internal/backend/remote-state/azure/go.sum @@ -157,6 +157,8 @@ github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk13 github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-slug v0.18.1 h1:UnWIy4mq9GaDr1LhAzCPgA6RSQUn952RLFqQe3HPyCs= @@ -179,6 +181,8 @@ github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTG github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.2.1 h1:ubvrTFw3Q7CsoEaX7V06PtCTKG3wu7GyyobAoN4eF3Q= github.com/hashicorp/terraform-svchost v0.2.1/go.mod h1:zDMheBLvNzu7Q6o9TBvPqiZToJcSuCLXjAXxBslSky4= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jackofallops/giovanni v0.28.0 h1:fxn55SnxL2Rj3hgkkgQS9UKlIRXkkTZ5WcnE04JCBRE= github.com/jackofallops/giovanni v0.28.0/go.mod h1:CyzRgZyts4YSI/1iZF8poqdn9I6J8xpmg1iMpvhthTs= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= @@ -210,6 +214,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/backend/remote-state/consul/go.sum b/internal/backend/remote-state/consul/go.sum index a388ddaafff9..440594601b63 100644 --- a/internal/backend/remote-state/consul/go.sum +++ b/internal/backend/remote-state/consul/go.sum @@ -133,6 +133,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -177,6 +179,8 @@ github.com/hashicorp/go-msgpack/v2 v2.1.2 h1:4Ee8FTp834e+ewB71RDrQ0VKpyFdrKOjvYt github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= @@ -204,6 +208,8 @@ github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTG github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.2.1 h1:ubvrTFw3Q7CsoEaX7V06PtCTKG3wu7GyyobAoN4eF3Q= github.com/hashicorp/terraform-svchost v0.2.1/go.mod h1:zDMheBLvNzu7Q6o9TBvPqiZToJcSuCLXjAXxBslSky4= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -247,6 +253,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/internal/backend/remote-state/cos/go.sum b/internal/backend/remote-state/cos/go.sum index 404710a4e714..91acad1bc631 100644 --- a/internal/backend/remote-state/cos/go.sum +++ b/internal/backend/remote-state/cos/go.sum @@ -103,6 +103,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -126,6 +128,8 @@ github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk13 github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-slug v0.18.1 h1:UnWIy4mq9GaDr1LhAzCPgA6RSQUn952RLFqQe3HPyCs= @@ -140,6 +144,8 @@ github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTG github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.2.1 h1:ubvrTFw3Q7CsoEaX7V06PtCTKG3wu7GyyobAoN4eF3Q= github.com/hashicorp/terraform-svchost v0.2.1/go.mod h1:zDMheBLvNzu7Q6o9TBvPqiZToJcSuCLXjAXxBslSky4= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= @@ -166,6 +172,8 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= github.com/mozillazg/go-httpheader v0.3.0 h1:3brX5z8HTH+0RrNA1362Rc3HsaxyWEKtGY45YrhuINM= github.com/mozillazg/go-httpheader v0.3.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/backend/remote-state/gcs/go.sum b/internal/backend/remote-state/gcs/go.sum index f84420aafcd5..ee53d0d71f66 100644 --- a/internal/backend/remote-state/gcs/go.sum +++ b/internal/backend/remote-state/gcs/go.sum @@ -137,6 +137,8 @@ github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk13 github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-slug v0.18.1 h1:UnWIy4mq9GaDr1LhAzCPgA6RSQUn952RLFqQe3HPyCs= @@ -151,6 +153,8 @@ github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTG github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.2.1 h1:ubvrTFw3Q7CsoEaX7V06PtCTKG3wu7GyyobAoN4eF3Q= github.com/hashicorp/terraform-svchost v0.2.1/go.mod h1:zDMheBLvNzu7Q6o9TBvPqiZToJcSuCLXjAXxBslSky4= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= @@ -167,6 +171,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/internal/backend/remote-state/kubernetes/go.sum b/internal/backend/remote-state/kubernetes/go.sum index 15e2f55a3c93..9a7db4a31a32 100644 --- a/internal/backend/remote-state/kubernetes/go.sum +++ b/internal/backend/remote-state/kubernetes/go.sum @@ -117,6 +117,8 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -141,6 +143,8 @@ github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk13 github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-slug v0.18.1 h1:UnWIy4mq9GaDr1LhAzCPgA6RSQUn952RLFqQe3HPyCs= @@ -155,6 +159,8 @@ github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTG github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.2.1 h1:ubvrTFw3Q7CsoEaX7V06PtCTKG3wu7GyyobAoN4eF3Q= github.com/hashicorp/terraform-svchost v0.2.1/go.mod h1:zDMheBLvNzu7Q6o9TBvPqiZToJcSuCLXjAXxBslSky4= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -193,6 +199,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= diff --git a/internal/backend/remote-state/oci/go.sum b/internal/backend/remote-state/oci/go.sum index b7ec6af3be61..987fb766fe27 100644 --- a/internal/backend/remote-state/oci/go.sum +++ b/internal/backend/remote-state/oci/go.sum @@ -103,6 +103,8 @@ github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.10.0 h1:SHMXenfaB03KbroETaCMtbBg3Yn29v4w1r+tgy4ff4k= github.com/gofrs/flock v0.10.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -121,6 +123,8 @@ github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk13 github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-slug v0.18.1 h1:UnWIy4mq9GaDr1LhAzCPgA6RSQUn952RLFqQe3HPyCs= @@ -135,6 +139,8 @@ github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTG github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.2.1 h1:ubvrTFw3Q7CsoEaX7V06PtCTKG3wu7GyyobAoN4eF3Q= github.com/hashicorp/terraform-svchost v0.2.1/go.mod h1:zDMheBLvNzu7Q6o9TBvPqiZToJcSuCLXjAXxBslSky4= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= @@ -151,6 +157,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oracle/oci-go-sdk/v65 v65.89.1 h1:8sVjxYPNQ83yqUgZKkdeUA0CnSodmL1Bme2oxq8gyKg= github.com/oracle/oci-go-sdk/v65 v65.89.1/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= diff --git a/internal/backend/remote-state/oss/go.sum b/internal/backend/remote-state/oss/go.sum index 4fd69216cd26..6f611ea91acc 100644 --- a/internal/backend/remote-state/oss/go.sum +++ b/internal/backend/remote-state/oss/go.sum @@ -131,6 +131,8 @@ github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk13 github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-slug v0.18.1 h1:UnWIy4mq9GaDr1LhAzCPgA6RSQUn952RLFqQe3HPyCs= @@ -145,6 +147,8 @@ github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTG github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.2.1 h1:ubvrTFw3Q7CsoEaX7V06PtCTKG3wu7GyyobAoN4eF3Q= github.com/hashicorp/terraform-svchost v0.2.1/go.mod h1:zDMheBLvNzu7Q6o9TBvPqiZToJcSuCLXjAXxBslSky4= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -189,6 +193,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= diff --git a/internal/backend/remote-state/pg/go.sum b/internal/backend/remote-state/pg/go.sum index 8a097d11349f..9fc3f823e1de 100644 --- a/internal/backend/remote-state/pg/go.sum +++ b/internal/backend/remote-state/pg/go.sum @@ -100,6 +100,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -118,6 +120,8 @@ github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk13 github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-slug v0.18.1 h1:UnWIy4mq9GaDr1LhAzCPgA6RSQUn952RLFqQe3HPyCs= @@ -132,6 +136,8 @@ github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTG github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.2.1 h1:ubvrTFw3Q7CsoEaX7V06PtCTKG3wu7GyyobAoN4eF3Q= github.com/hashicorp/terraform-svchost v0.2.1/go.mod h1:zDMheBLvNzu7Q6o9TBvPqiZToJcSuCLXjAXxBslSky4= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= @@ -150,6 +156,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/backend/remote-state/s3/go.sum b/internal/backend/remote-state/s3/go.sum index 35ac311e156f..11c368e8a99a 100644 --- a/internal/backend/remote-state/s3/go.sum +++ b/internal/backend/remote-state/s3/go.sum @@ -115,6 +115,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -138,6 +140,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-slug v0.18.1 h1:UnWIy4mq9GaDr1LhAzCPgA6RSQUn952RLFqQe3HPyCs= @@ -154,6 +158,8 @@ github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTG github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.2.1 h1:ubvrTFw3Q7CsoEaX7V06PtCTKG3wu7GyyobAoN4eF3Q= github.com/hashicorp/terraform-svchost v0.2.1/go.mod h1:zDMheBLvNzu7Q6o9TBvPqiZToJcSuCLXjAXxBslSky4= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= @@ -172,6 +178,8 @@ github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJ github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/plans/plan.go b/internal/plans/plan.go index 3b1b2a88c595..921f94362a0b 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -169,6 +169,10 @@ type Plan struct { // and builtin calls which may access external state so that calls during // apply can be checked for consistency. FunctionResults []lang.FunctionResultHash + + // PolicyResults stores the results of policy evaluations that were performed + // while making this plan. + PolicyResults *PolicyResults } // ProviderAddrs returns a list of all of the provider configuration addresses diff --git a/internal/plans/policy.go b/internal/plans/policy.go new file mode 100644 index 000000000000..1000510b70cf --- /dev/null +++ b/internal/plans/policy.go @@ -0,0 +1,106 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package plans + +import ( + "iter" + "sync" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/policy" +) + +// PolicyResults represents the results of policy evaluation of resources, modules, and providers for a single plan. +type PolicyResults struct { + mu *sync.Mutex + set addrs.Map[addrs.AbsResourceInstance, PolicyEvaluation] + pset addrs.Map[addrs.AbsProviderConfig, PolicyEvaluation] + mset addrs.Map[addrs.Module, PolicyEvaluation] +} + +// PolicyEvaluation holds the result of a policy evaluation for a single resource, module, or provider. +type PolicyEvaluation struct { + EvaluationResponse policy.EvaluationResponse + ConfigDeclRange hcl.Range +} + +func NewPolicyResults() *PolicyResults { + return &PolicyResults{ + mu: &sync.Mutex{}, + set: addrs.MakeMap[addrs.AbsResourceInstance, PolicyEvaluation](), + pset: addrs.MakeMap[addrs.AbsProviderConfig, PolicyEvaluation](), + mset: addrs.MakeMap[addrs.Module, PolicyEvaluation](), + } +} + +func (pr *PolicyResults) AddResource(addr addrs.AbsResourceInstance, result policy.EvaluationResponse, config *configs.Resource) { + // Don't add empty results + if result.Empty() { + return + } + pr.mu.Lock() + defer pr.mu.Unlock() + var rng hcl.Range + if config != nil { + rng = config.DeclRange + } + pr.set.Put(addr, PolicyEvaluation{EvaluationResponse: result, ConfigDeclRange: rng}) +} + +func (pr *PolicyResults) AddProvider(addr addrs.AbsProviderConfig, result policy.EvaluationResponse, config *configs.Provider) { + // Don't add empty results + if result.Empty() { + return + } + pr.mu.Lock() + defer pr.mu.Unlock() + var rng hcl.Range + if config != nil { + rng = config.DeclRange + } + pr.pset.Put(addr, PolicyEvaluation{EvaluationResponse: result, ConfigDeclRange: rng}) +} + +func (pr *PolicyResults) AddModule(addr addrs.Module, result policy.EvaluationResponse, config *configs.ModuleCall) { + // Don't add empty results + if result.Empty() { + return + } + pr.mu.Lock() + defer pr.mu.Unlock() + var rng hcl.Range + if config != nil { + rng = config.DeclRange + } + pr.mset.Put(addr, PolicyEvaluation{EvaluationResponse: result, ConfigDeclRange: rng}) +} + +func (pr *PolicyResults) Iter() iter.Seq2[string, PolicyEvaluation] { + return func(yield func(string, PolicyEvaluation) bool) { + for k, v := range pr.set.Iter() { + if !yield(k.String(), v) { + return + } + } + for k, v := range pr.pset.Iter() { + if !yield(k.String(), v) { + return + } + } + for k, v := range pr.mset.Iter() { + if !yield(k.String(), v) { + return + } + } + } +} + +func (pr *PolicyResults) Len() int { + if pr == nil { + return 0 + } + return pr.set.Len() + pr.pset.Len() + pr.mset.Len() +} diff --git a/internal/plans/policy_test.go b/internal/plans/policy_test.go new file mode 100644 index 000000000000..4f0904e96974 --- /dev/null +++ b/internal/plans/policy_test.go @@ -0,0 +1,81 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package plans + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/policy" +) + +func TestPolicyResults(t *testing.T) { + resourceAddr, _ := addrs.ParseAbsResourceInstanceStr("test_instance.example") + providerAddr, _ := addrs.ParseAbsProviderConfigStr("provider[\"registry.terraform.io/hashicorp/test\"]") + moduleAddr := addrs.Module{"child"} + resourceConfig := &configs.Resource{DeclRange: hcl.Range{Filename: "resource.tf", Start: hcl.Pos{Line: 10, Column: 1}, End: hcl.Pos{Line: 10, Column: 20}}} + providerConfig := &configs.Provider{DeclRange: hcl.Range{Filename: "provider.tf", Start: hcl.Pos{Line: 20, Column: 1}, End: hcl.Pos{Line: 20, Column: 20}}} + moduleConfig := &configs.ModuleCall{DeclRange: hcl.Range{Filename: "module.tf", Start: hcl.Pos{Line: 30, Column: 1}, End: hcl.Pos{Line: 30, Column: 20}}} + + t.Run("empty result", func(t *testing.T) { + pr := NewPolicyResults() + // this is an empty result because it contains no diagnostics or enforcements + allow := policy.EvaluationResponse{Overall: policy.AllowResult} + + pr.AddResource(resourceAddr, allow, resourceConfig) + pr.AddProvider(providerAddr, allow, providerConfig) + pr.AddModule(moduleAddr, allow, moduleConfig) + + // Empty results should be skipped, so the length should still be 0 + if got := pr.Len(); got != 0 { + t.Fatalf("unexpected number of stored results: got %d, want 0", got) + } + }) + + t.Run("Add non-empty result", func(t *testing.T) { + pr := NewPolicyResults() + resourceResult := policy.EvaluationResponse{Overall: policy.DenyResult} + providerResult := policy.EvaluationResponse{Overall: policy.PolicyErrorResult} + moduleResult := policy.EvaluationResponse{Overall: policy.DenyResult} + + pr.AddResource(resourceAddr, resourceResult, resourceConfig) + pr.AddProvider(providerAddr, providerResult, providerConfig) + pr.AddModule(moduleAddr, moduleResult, moduleConfig) + + if got := pr.Len(); got != 3 { + t.Fatalf("unexpected number of stored results: got %d, want 3", got) + } + + got := map[string]PolicyEvaluation{} + for addr, result := range pr.Iter() { + got[addr] = result + } + + want := map[string]PolicyEvaluation{ + resourceAddr.String(): { + EvaluationResponse: resourceResult, + ConfigDeclRange: resourceConfig.DeclRange, + }, + providerAddr.String(): { + EvaluationResponse: providerResult, + ConfigDeclRange: providerConfig.DeclRange, + }, + moduleAddr.String(): { + EvaluationResponse: moduleResult, + ConfigDeclRange: moduleConfig.DeclRange, + }, + } + + if len(got) != len(want) { + t.Fatalf("unexpected number of iterated results: got %d, want %d", len(got), len(want)) + } + + if diff := cmp.Diff(got, want); diff != "" { + t.Fatalf("unexpected results: %s", diff) + } + }) +} diff --git a/internal/policy/policy.go b/internal/policy/policy.go index 072d81e175be..04a923c3b78f 100644 --- a/internal/policy/policy.go +++ b/internal/policy/policy.go @@ -181,6 +181,7 @@ func EvaluationFromProtoResponse(overall proto.EvaluateResult, policyDetails []* return ret } +// Empty returns true if the response indicates that the policy engine has no matched policy for the object. func (r EvaluationResponse) Empty() bool { // The policy engine sends an allow result when the object has no matched policy, consequently // impliciting allowing it. However, such object really had no policy, and may not need to be rendered. diff --git a/internal/providers/testing/provider_mock.go b/internal/providers/testing/provider_mock.go index 6dae6aa59afb..5552746da4da 100644 --- a/internal/providers/testing/provider_mock.go +++ b/internal/providers/testing/provider_mock.go @@ -674,6 +674,15 @@ func (p *MockProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) return resp } + // this resource is deferred + if r.ProposedNewState.Type().HasAttribute("defer") { + if shouldBeDeferred := r.ProposedNewState.GetAttr("defer"); !shouldBeDeferred.IsKnown() || (!shouldBeDeferred.IsNull() && shouldBeDeferred.True()) { + resp.Deferred = &providers.Deferred{ + Reason: providers.DeferredReasonResourceConfigUnknown, + } + } + } + schema, ok := p.getProviderSchema().ResourceTypes[r.TypeName] if !ok { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("no schema found for %q", r.TypeName)) diff --git a/internal/terraform/context_apply.go b/internal/terraform/context_apply.go index 98432c21b736..8c812a41e670 100644 --- a/internal/terraform/context_apply.go +++ b/internal/terraform/context_apply.go @@ -12,8 +12,10 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -46,6 +48,14 @@ type ApplyOpts struct { // or test runtimes, where the root modules as Terraform sees them aren't // the actual root modules. AllowRootEphemeralOutputs bool + + // Locks is a read-only snapshot of provider locks (from the dependency lock + // file). + Locks map[addrs.Provider]*depsfile.ProviderLock + + // Optional policy client to enable live policy evaluations. + PolicyClient policy.Client + PolicyResults *plans.PolicyResults } // ApplyOpts creates an [ApplyOpts] with copies of all of the elements that @@ -61,6 +71,7 @@ func (po *PlanOpts) ApplyOpts() *ApplyOpts { return &ApplyOpts{ ExternalProviders: po.ExternalProviders, AllowRootEphemeralOutputs: po.AllowRootEphemeralOutputs, + Locks: po.Locks, } } @@ -207,6 +218,10 @@ func (c *Context) ApplyAndEval(plan *plans.Plan, config *configs.Config, opts *A PlanTimeTimestamp: plan.Timestamp, FunctionResults: lang.NewFunctionResultsTable(plan.FunctionResults), + + Locks: opts.Locks, + PolicyClient: opts.PolicyClient, + PolicyResults: opts.PolicyResults, }) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) @@ -378,6 +393,7 @@ func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, opts *App Overrides: plan.Overrides, SkipGraphValidation: c.graphOpts.SkipGraphValidation, AllowRootEphemeralOutputs: opts.AllowRootEphemeralOutputs, + PolicyClient: opts.PolicyClient, }).Build(addrs.RootModuleInstance) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { diff --git a/internal/terraform/context_apply_policy_test.go b/internal/terraform/context_apply_policy_test.go new file mode 100644 index 000000000000..fe5cf9e75ff3 --- /dev/null +++ b/internal/terraform/context_apply_policy_test.go @@ -0,0 +1,787 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/policy" + "github.com/hashicorp/terraform/internal/policy/proto" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestContext2Apply_PolicyEvaluation_Full(t *testing.T) { + mainConfig := ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + variable "input" { + type = string + default = "foo" + } + + resource "test_resource" "test" { + sensitive_value = "foo" + } + + module "child" { + source = "./child" + // this is a computed value in the parent, so will not be available until apply. + input = test_resource.test.id + } + + ` + childConfig := ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + variable "input" { + type = string + } + + resource "test_instance" "child" { + value = var.input + } + + ` + policyConfig := ` + resource_policy "test_resource" "policy_name" { + enforce { + condition = attrs.sensitive_value == "foo" + } + } + ` + configFiles := map[string]string{ + "main.tf": mainConfig, + "child/child.tf": childConfig, + "main.tfpolicy.hcl": policyConfig, + } + + mod := testModuleInline(t, configFiles) + providerAddr := addrs.NewDefaultProvider("test") + provider := testProvider("test") + + provider.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + cfg := req.Config.AsValueMap() + if req.TypeName == "test_resource" { + cfg["id"] = cty.StringVal("parent") + } + resp.NewState = cty.ObjectVal(cfg) + return resp + } + state := states.NewState() + + // mock the policy expectations during plan + planPolicyClient := policy.NewTestMockClient(t) + + // The expected values to be sent for policy evaluation. + expectedPlan := map[string]cty.Value{ + "test_resource": cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + "sensitive_value": cty.StringVal("foo"), + }), + "test_instance": cty.ObjectVal(map[string]cty.Value{ + "value": cty.UnknownVal(cty.String), + "sensitive_value": cty.NilVal, + }), + } + actualPlan := make(map[string]cty.Value) + + planPolicyClient.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + var actualVal cty.Value + attrs := req.Attrs + target := req.Target + if !attrs.IsNull() { + mp := attrs.AsValueMap() + actualVal = cty.ObjectVal(map[string]cty.Value{ + "value": mp["value"], + "sensitive_value": mp["sensitive_value"], + }) + } + actualPlan[target] = actualVal + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + t.Cleanup(func() { + if diff := cmp.Diff(actualPlan, expectedPlan, cmp.Comparer(cty.Value.RawEquals)); diff != "" { + t.Errorf("Unexpected diff (-got +want):\n%s", diff) + } + }) + + ctx, diags := NewContext(&ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + providerAddr: testProviderFuncFixed(provider), + }, + Parallelism: 1, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + plan, diags := ctx.Plan(mod, state, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: testInputValuesUnset(mod.Module.Variables), + PolicyClient: planPolicyClient, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + // mock the policy expectations during apply + applyPolicyClient := policy.NewTestMockClient(t) + + // The expected values to be sent for policy evaluation. + expectedApply := map[string]cty.Value{ + "test_resource": cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + "sensitive_value": cty.StringVal("foo"), + }), + "test_instance": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("parent"), // was unknown in the plan + "sensitive_value": cty.NilVal, + }), + } + actualApply := make(map[string]cty.Value) + + applyPolicyClient.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + var actual cty.Value + attrs := req.Attrs + target := req.Target + if !attrs.IsNull() { + mp := attrs.AsValueMap() + actual = cty.ObjectVal(map[string]cty.Value{ + "value": mp["value"], + "sensitive_value": mp["sensitive_value"], + }) + } + actualApply[target] = actual + + // this return does not actually do anything + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + t.Cleanup(func() { + if diff := cmp.Diff(actualApply, expectedApply, cmp.Comparer(cty.Value.RawEquals)); diff != "" { + t.Errorf("Unexpected diff (-got +want):\n%s", diff) + } + }) + + _, diags = ctx.Apply(plan, mod, &ApplyOpts{ + PolicyClient: applyPolicyClient, + }) + tfdiags.AssertNoDiagnostics(t, diags) + +} + +// TestContext2Apply_PolicyEvaluationError tests that the apply operation returns policy diagnostics +// when the policy evaluation returns an error. +func TestContext2Apply_PolicyEvaluationError(t *testing.T) { + mainConfig := ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + variable "input" { + type = string + default = "foo" + } + + resource "test_resource" "test" { + sensitive_value = "foo" + } + + module "child" { + source = "./child" + // this is a computed value in the parent, so will not be available until apply. + input = test_resource.test.id + } + + ` + childConfig := ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + variable "input" { + type = string + } + + resource "test_instance" "child" { + value = var.input + } + + ` + policyConfig := ` + resource_policy "test_resource" "policy_name" { + enforce { + condition = attrs.sensitive_value == "foo" + } + } + ` + configFiles := map[string]string{ + "main.tf": mainConfig, + "child/child.tf": childConfig, + "main.tfpolicy.hcl": policyConfig, + } + + mod := testModuleInline(t, configFiles) + providerAddr := addrs.NewDefaultProvider("test") + provider := testProvider("test") + + provider.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + cfg := req.Config.AsValueMap() + if req.TypeName == "test_resource" { + cfg["id"] = cty.StringVal("parent") + } + resp.NewState = cty.ObjectVal(cfg) + return resp + } + state := states.NewState() + + // mock the policy expectations during plan + planPolicyClient := policy.NewTestMockClient(t) + + // The expected values to be sent for policy evaluation. + expected := map[string]cty.Value{ + "test_resource": cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + "sensitive_value": cty.StringVal("foo"), + }), + "test_instance": cty.ObjectVal(map[string]cty.Value{ + "value": cty.UnknownVal(cty.String), + "sensitive_value": cty.NilVal, + }), + } + + planPolicyClient.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + var actual cty.Value + attrs := req.Attrs + target := req.Target + if !attrs.IsNull() { + mp := attrs.AsValueMap() + actual = cty.ObjectVal(map[string]cty.Value{ + "value": mp["value"], + "sensitive_value": mp["sensitive_value"], + }) + } + + if diff := cmp.Diff(actual, expected[target], cmp.Comparer(cty.Value.RawEquals)); diff != "" { + t.Errorf("Unexpected diff (-got +want):\n%s", diff) + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + ctx, diags := NewContext(&ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + providerAddr: testProviderFuncFixed(provider), + }, + Parallelism: 1, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + plan, diags := ctx.Plan(mod, state, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: testInputValuesUnset(mod.Module.Variables), + PolicyClient: planPolicyClient, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + // mock the policy expectations during apply + applyPolicyClient := policy.NewTestMockClient(t) + + // The expected values to be sent for policy evaluation. + expected = map[string]cty.Value{ + "test_resource": cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + "sensitive_value": cty.StringVal("foo"), + }), + "test_instance": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("parent"), // was unknown in the plan + "sensitive_value": cty.NilVal, + }), + } + + // Track which resource we're evaluating for different responses + applyPolicyClient.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + var actual cty.Value + attrs := req.Attrs + target := req.Target + if !attrs.IsNull() { + mp := attrs.AsValueMap() + actual = cty.ObjectVal(map[string]cty.Value{ + "value": mp["value"], + "sensitive_value": mp["sensitive_value"], + }) + } + + if diff := cmp.Diff(actual, expected[target], cmp.Comparer(cty.Value.RawEquals)); diff != "" { + t.Errorf("Unexpected diff (-got +want):\n%s", diff) + } + + if target == "test_resource" { + return policy.EvaluationResponse{ + Overall: policy.DenyResult, + Diagnostics: policy.DiagsFromProto([]*proto.Diagnostic{ + { + Severity: proto.Severity_ERROR, + Summary: "error message", + }, + }, nil), + } + } + + // test_instance should still be evaluated despite the error in test_resource + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + applyResults := plans.NewPolicyResults() + state, diags = ctx.Apply(plan, mod, &ApplyOpts{ + PolicyClient: applyPolicyClient, + PolicyResults: applyResults, + }) + tfdiags.AssertDiagnosticCount(t, diags, 0) + + var policyDiags tfdiags.Diagnostics + for _, res := range applyResults.Iter() { + policyDiags = policyDiags.Append(res.EvaluationResponse.Diagnostics.AsTerraformDiags()) + } + var exp tfdiags.Diagnostics + exp = exp.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "error message", + Subject: policyDiags[0].Source().Subject.ToHCL().Ptr(), + Extra: policyDiags[0].ExtraInfo(), + }) + tfdiags.AssertDiagnosticsMatch(t, policyDiags, exp) + + addrs := state.AllManagedResourceInstanceObjectAddrs() + if len(addrs) != 2 { + t.Fatalf("expected 1 managed resource in the state, got %d", len(addrs)) + } + + rs := state.Resource(mustAbsResourceAddr("test_resource.test")) + if rs == nil { + t.Fatal("expected resource to be in the state") + } +} + +func TestContext2Apply_PolicyEvaluation_NoResourceAfterPolicy(t *testing.T) { + // This verifies that no resource instance node is run after policy evaluation + mainConfig := ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + resource "test_instance" "test" { + count = 2 + value = tostring(count.index) + } + ` + + policyConfig := ` + resource_policy "test_instance" "policy_name" { + enforce { + condition = true + } + } + ` + + mod := testModuleInline(t, map[string]string{ + "main.tf": mainConfig, + "main.tfpolicy.hcl": policyConfig, + }) + + providerAddr := addrs.NewDefaultProvider("test") + provider := testProvider("test") + + var policyRan atomic.Bool + var applyCalls atomic.Int32 + + provider.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + callNum := applyCalls.Add(1) + if callNum == 2 { + time.Sleep(150 * time.Millisecond) + } + + if policyRan.Load() { + t.Fatalf("resource apply for %s ran after policy evaluation", req.TypeName) + } + + newState := req.PlannedState.AsValueMap() + newState["id"] = cty.StringVal(req.PlannedState.GetAttr("value").AsString()) + newState["type"] = cty.StringVal(req.TypeName) + newState["unknown"] = cty.StringVal("known") + resp.NewState = cty.ObjectVal(newState) + return resp + } + + ctx, diags := NewContext(&ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + providerAddr: testProviderFuncFixed(provider), + }, + Parallelism: 4, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + plan, diags := ctx.Plan(mod, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: testInputValuesUnset(mod.Module.Variables), + }) + tfdiags.AssertNoDiagnostics(t, diags) + + applyPolicyClient := policy.NewTestMockClient(t) + applyPolicyClient.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + policyRan.Store(true) + + if diff := cmp.Diff(req.Meta, &proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: "test", + Operation: proto.Operation_CREATE, + }, protocmp.Transform()); diff != "" { + t.Errorf("Invalid resource metadata: %s", diff) + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + resultState, diags := ctx.Apply(plan, mod, &ApplyOpts{ + PolicyClient: applyPolicyClient, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + if !applyPolicyClient.EvaluateCalled { + t.Fatal("expected policy evaluation to be called during apply") + } + + remainingAddrs := resultState.AllManagedResourceInstanceObjectAddrs() + if len(remainingAddrs) != 2 { + t.Fatalf("expected 2 managed resources in the state after apply, got %d: %v", len(remainingAddrs), remainingAddrs) + } +} + +func TestContext2Apply_PolicyEvaluation_ChangedResourceCount(t *testing.T) { + cases := []struct { + name string + state *states.State + configBody string + expectTarget string + expectOp proto.Operation + expectCalls int + expectFinalAttr cty.Value + }{ + { + name: "create", + state: states.NewState(), + configBody: ` +resource "test_resource" "test" { + sensitive_value = "foo" +} +`, + expectTarget: "test_resource", + expectOp: proto.Operation_CREATE, + expectCalls: 1, + expectFinalAttr: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("created"), + "sensitive_value": cty.StringVal("foo"), + }), + }, + { + name: "update", + state: states.BuildState(func(ss *states.SyncState) { + ss.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_resource.test"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"existing","type":"test_resource","sensitive_value":"before"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + }), + configBody: ` +resource "test_resource" "test" { + sensitive_value = "after" +} +`, + expectTarget: "test_resource", + expectOp: proto.Operation_UPDATE, + expectCalls: 1, + expectFinalAttr: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("existing"), + "sensitive_value": cty.StringVal("after"), + }), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + mainConfig := ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } +` + tc.configBody + + policyConfig := ` + resource_policy "test_resource" "policy_name" { + enforce { + condition = true + } + } +` + mod := testModuleInline(t, map[string]string{ + "main.tf": mainConfig, + "main.tfpolicy.hcl": policyConfig, + }) + + providerAddr := addrs.NewDefaultProvider("test") + provider := testProvider("test") + provider.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + cfg := req.Config.AsValueMap() + if req.TypeName == "test_resource" { + if id, ok := cfg["id"]; ok && !id.IsNull() && id.IsKnown() { + cfg["id"] = id + } else if tc.name == "create" { + cfg["id"] = cty.StringVal("created") + } else { + cfg["id"] = cty.StringVal("existing") + } + } + resp.NewState = cty.ObjectVal(cfg) + return resp + } + + ctx, diags := NewContext(&ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + providerAddr: testProviderFuncFixed(provider), + }, + Parallelism: 1, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + planPolicyClient := policy.NewTestMockClient(t) + plan, diags := ctx.Plan(mod, tc.state, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: testInputValuesUnset(mod.Module.Variables), + PolicyClient: planPolicyClient, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + applyPolicyClient := policy.NewTestMockClient(t) + var called int + applyPolicyClient.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + called++ + if req.Target != tc.expectTarget { + t.Fatalf("expected target %s, got %s", tc.expectTarget, req.Target) + } + if diff := cmp.Diff(req.Meta, &proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: "test", + Operation: tc.expectOp, + }, protocmp.Transform()); diff != "" { + t.Fatalf("unexpected resource metadata (-got +want):\n%s", diff) + } + + actualAttrs := req.Attrs + if !actualAttrs.IsNull() { + mp := actualAttrs.AsValueMap() + actualAttrs = cty.ObjectVal(map[string]cty.Value{ + "id": mp["id"], + "sensitive_value": mp["sensitive_value"], + }) + } + if diff := cmp.Diff(actualAttrs, tc.expectFinalAttr, cmp.Comparer(cty.Value.RawEquals)); diff != "" { + t.Fatalf("unexpected attrs (-got +want):\n%s", diff) + } + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + _, diags = ctx.Apply(plan, mod, &ApplyOpts{ + PolicyClient: applyPolicyClient, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + if called != tc.expectCalls { + t.Fatalf("expected %d policy evaluation call(s), got %d", tc.expectCalls, called) + } + }) + } +} + +func TestContext2Apply_PolicyEvaluation_Destroy(t *testing.T) { + mainConfig := ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + resource "test_resource" "test" { + sensitive_value = "foo" + } + ` + policyConfig := ` + resource_policy "test_resource" "policy_name" { + enforce { + condition = true + } + } + ` + configFiles := map[string]string{ + "main.tf": mainConfig, + "main.tfpolicy.hcl": policyConfig, + } + + mod := testModuleInline(t, configFiles) + providerAddr := addrs.NewDefaultProvider("test") + provider := testProvider("test") + + // Build a pre-existing state with the resource already created. + state := states.BuildState(func(ss *states.SyncState) { + ss.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_resource.test"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar","sensitive_value":"foo"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + }) + + planPolicyClient := policy.NewTestMockClient(t) + var planEvalCalled int + + planPolicyClient.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + planEvalCalled++ + + if req.Target != "test_resource" { + t.Errorf("Plan: expected target to be test_resource, got %s", req.Target) + } + + // For a destroy plan, attrs (the "after" value) should be null. + if !req.Attrs.IsNull() { + t.Errorf("Plan: expected null attrs for destroy evaluation, got %#v", req.Attrs) + } + + // PriorAttrs should contain the state being destroyed. + if req.PriorAttrs.IsNull() { + t.Errorf("Plan: expected non-null PriorAttrs for destroy evaluation") + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + ctx, diags := NewContext(&ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + providerAddr: testProviderFuncFixed(provider), + }, + Parallelism: 1, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + plan, diags := ctx.Plan(mod, state, &PlanOpts{ + Mode: plans.DestroyMode, + SetVariables: testInputValuesUnset(mod.Module.Variables), + PolicyClient: planPolicyClient, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + if planEvalCalled != 1 { + t.Fatalf("Plan: expected policy Evaluate to be called 1 time, got %d", planEvalCalled) + } + + // Verify the plan contains a delete action. + var foundDelete bool + for _, rc := range plan.Changes.Resources { + if rc.Addr.String() == "test_resource.test" { + if rc.Action != plans.Delete { + t.Errorf("Expected delete action for test_resource.test, got %s", rc.Action) + } + foundDelete = true + } + } + if !foundDelete { + t.Fatal("Expected test_resource.test in plan changes") + } + + // --- Apply phase --- + applyPolicyClient := policy.NewTestMockClient(t) + var applyEvalCalled int + + applyPolicyClient.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + applyEvalCalled++ + + if req.Target != "test_resource" { + t.Errorf("Apply: expected target to be test_resource, got %s", req.Target) + } + + if !req.Attrs.IsNull() { + t.Errorf("Apply: expected null attrs for destroy evaluation, got %#v", req.Attrs) + } + + if req.PriorAttrs.IsNull() { + t.Errorf("Apply: expected non-null PriorAttrs for destroy evaluation") + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + resultState, diags := ctx.Apply(plan, mod, &ApplyOpts{ + PolicyClient: applyPolicyClient, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + if applyEvalCalled != 1 { + t.Fatalf("Apply: expected policy Evaluate to be called 1 time, got %d", applyEvalCalled) + } + + // After a successful destroy, the resource should no longer be in state. + remainingAddrs := resultState.AllManagedResourceInstanceObjectAddrs() + if len(remainingAddrs) != 0 { + t.Fatalf("expected 0 managed resources in the state after destroy, got %d: %v", len(remainingAddrs), remainingAddrs) + } + + rs := resultState.Resource(mustAbsResourceAddr("test_resource.test")) + if rs != nil { + t.Fatal("expected test_resource.test to be removed from state after destroy") + } +} diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index 3cdff8277619..959ff7e2c301 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -16,11 +16,13 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/globalref" "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/refactoring" "github.com/hashicorp/terraform/internal/states" @@ -160,6 +162,13 @@ type PlanOpts struct { // or test runtimes, where the root modules as Terraform sees them aren't // the actual root modules. AllowRootEphemeralOutputs bool + + // Locks is a read-only snapshot of provider locks (from the dependency lock + // file). + Locks map[addrs.Provider]*depsfile.ProviderLock + + // Optional policy client to enable live policy evaluations. + PolicyClient policy.Client } // Plan generates an execution plan by comparing the given configuration @@ -793,6 +802,9 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o // Hold reference to this so we can store the table data in the plan file. funcResults := lang.NewFunctionResultsTable(nil) + // Initialize the map to store policy evaluation results. + policyResults := plans.NewPolicyResults() + walker, walkDiags := c.walk(graph, walkOp, &graphWalkOpts{ Config: config, InputState: prevRunState, @@ -805,6 +817,9 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o PlanTimeTimestamp: timestamp, FunctionResults: funcResults, Forget: opts.Forget, + Locks: opts.Locks, + PolicyClient: opts.PolicyClient, + PolicyResults: policyResults, }) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) @@ -883,10 +898,13 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o Checks: states.NewCheckResults(walker.Checks), Timestamp: timestamp, FunctionResults: funcResults.GetHashes(), - // Other fields get populated by Context.Plan after we return } + if policyResults != nil { + plan.PolicyResults = policyResults + } + if !schemaDiags.HasErrors() { deferredResources, deferredDiags := c.deferredResources(schemas, walker.Deferrals.GetDeferredChanges()) diags = diags.Append(deferredDiags) @@ -1022,6 +1040,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, queryPlan: opts.Query, overridePreventDestroy: opts.OverridePreventDestroy, AllowRootEphemeralOutputs: opts.AllowRootEphemeralOutputs, + PolicyClient: opts.PolicyClient, }).Build(addrs.RootModuleInstance) return graph, walkPlan, diags case plans.RefreshOnlyMode: @@ -1040,6 +1059,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, Overrides: opts.Overrides, SkipGraphValidation: c.graphOpts.SkipGraphValidation, AllowRootEphemeralOutputs: opts.AllowRootEphemeralOutputs, + PolicyClient: opts.PolicyClient, }).Build(addrs.RootModuleInstance) return graph, walkPlan, diags case plans.DestroyMode: @@ -1056,6 +1076,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, SkipGraphValidation: c.graphOpts.SkipGraphValidation, overridePreventDestroy: opts.OverridePreventDestroy, AllowRootEphemeralOutputs: opts.AllowRootEphemeralOutputs, + PolicyClient: opts.PolicyClient, }).Build(addrs.RootModuleInstance) return graph, walkPlanDestroy, diags default: diff --git a/internal/terraform/context_plan_policy_test.go b/internal/terraform/context_plan_policy_test.go new file mode 100644 index 000000000000..69a7016cd475 --- /dev/null +++ b/internal/terraform/context_plan_policy_test.go @@ -0,0 +1,1341 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "context" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/policy" + "github.com/hashicorp/terraform/internal/policy/proto" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestContext2Plan_PolicyEvaluation(t *testing.T) { + type data struct { + config *configs.Config + plan *plans.Plan + state *states.State + diags tfdiags.Diagnostics + policy *policy.MockClient + policyEvalCalls int + } + cases := []struct { + name string + mainConfig string + childConfig string + policyConfig string + state *states.State + planMode plans.Mode + forceReplace []addrs.AbsResourceInstance + deferralAllowed bool + expectCalls int + prepareExpectations func(*testing.T, *data) + assertPolicyResults func(*testing.T, *data) + }{ + { + name: "make policy evaluation calls", + expectCalls: 2, + mainConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + variable "input" { + type = string + default = "foo" + } + + variable "input2" { + type = string + default = "bar" + } + + resource "test_resource" "test" { + sensitive_value = "foo" + } + + module "child" { + source = "./child" + } + + `, + childConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + variable "input" { + type = string + default = "child-foo" + } + + resource "test_instance" "test" { + value = "foo" + } + + `, + policyConfig: ` + resource_policy "test_resource" "policy_name" { + enforce { + condition = attrs.sensitive_value == "foo" + } + } + `, + prepareExpectations: func(t *testing.T, data *data) { + + // The expected values to be sent for policy evaluation. + expected := map[string]cty.Value{ + "test_resource": cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + "sensitive_value": cty.StringVal("foo"), + }), + + "test_instance": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("foo"), + }), + } + data.policy.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + data.policyEvalCalls++ + var actual cty.Value + if !req.Attrs.IsNull() { + mp := req.Attrs.AsValueMap() + retMP := map[string]cty.Value{ + "value": mp["value"], + } + if sv, ok := mp["sensitive_value"]; ok { + retMP["sensitive_value"] = sv + } + actual = cty.ObjectVal(retMP) + } + + if diff := cmp.Diff(actual, expected[req.Target], cmp.Comparer(cty.Value.RawEquals)); diff != "" { + t.Errorf("Unexpected diff (-got +want):\n%s", diff) + } + + if diff := cmp.Diff(req.Meta, &proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: "test", + Operation: proto.Operation_CREATE, + }, protocmp.Transform()); diff != "" { + t.Errorf("Invalid resource metadata: %s", diff) + } + + // Both resources are being created, so PriorAttrs should be null. + if !req.PriorAttrs.IsNull() { + t.Errorf("Expected null PriorAttrs for newly created %s, got non-null", req.Target) + return policy.EvaluationResponse{} + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + data.policy.EvaluateModuleFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateModuleRequest_ModuleMetadata]) policy.EvaluationResponse { + if req.Meta != nil { + if req.Meta.Address != "module.child" { + t.Errorf(`Expected module address to be "module.child", got "%s"`, req.Meta.Address) + } + } + + if req.Target != "./child" { + t.Errorf(`Expected target to be "./child", got %s`, req.Target) + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + }, + assertPolicyResults: func(t *testing.T, d *data) { + if !d.policy.EvaluateProviderCalled { + t.Error("Expected policyClient.EvaluateProvider to be called") + } + if !d.policy.EvaluateModuleCalled { + t.Error("Expected policyClient.EvaluateModule to be called") + } + if !d.policy.EvaluateCalled { + t.Error("Expected policyClient.Evaluate to be called") + } + tfdiags.AssertNoDiagnostics(t, d.diags) + }, + }, + + { + name: "deferred resource: policy is skipped", + expectCalls: 0, + mainConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + variable "input" { + type = string + default = "foo" + } + + variable "input2" { + type = string + default = "bar" + } + + resource "test_resource" "test" { + sensitive_value = "foo" + defer = true + } + `, + childConfig: "", + policyConfig: ` + resource_policy "test_resource" "policy_name" { + enforce { + condition = attrs.sensitive_value == "foo" + } + } + `, + deferralAllowed: true, + prepareExpectations: func(t *testing.T, data *data) { + data.policy.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + t.Fatalf("Expected policy evaluation to be skipped for deferred resource, but got request for %s", req.Target) + return policy.EvaluationResponse{} + } + }, + assertPolicyResults: func(t *testing.T, d *data) { + if d.policy.EvaluateCalled { + t.Error("Expected policyClient.Evaluate not to be called for deferred resource") + } + tfdiags.AssertNoDiagnostics(t, d.diags) + + if len(d.plan.DeferredResources) != 1 { + t.Fatalf("Expected 1 deferred resource, got %d", len(d.plan.DeferredResources)) + } + }, + }, + { + name: "orphaned resource instance: policy is evaluated", + expectCalls: 1, + mainConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + variable "input" { + type = string + default = "foo" + } + + variable "input2" { + type = string + default = "bar" + } + + resource "test_resource" "test" { + sensitive_value = "foo" + } + `, + childConfig: "", + policyConfig: ` + resource_policy "test_resource" "policy_name" { + enforce { + condition = attrs.sensitive_value == "foo" + } + } + `, + state: states.BuildState(func(ss *states.SyncState) { + testAddr := mustResourceInstanceAddr("test_resource.test") + orphanAddr := mustResourceInstanceAddr("test_instance.child") + ss.SetResourceInstanceCurrent( + testAddr, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bin","type":"test_resource","sensitive_value":"foo"}`), + Dependencies: []addrs.ConfigResource{ + orphanAddr.ContainingResource().Config(), + }, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + ss.SetResourceInstanceCurrent( + orphanAddr, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bin","type":"test_instance","sensitive_value":"foo-child"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + }), + prepareExpectations: func(t *testing.T, data *data) { + // The expected values to be sent for policy evaluation. + expected := map[string]cty.Value{ + "test_resource": cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + "sensitive_value": cty.StringVal("foo"), + }), + + // orphaned resource, so a nil set would be sent for policy evaluation. + "test_instance": cty.NilVal, + } + + data.policy.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + data.policyEvalCalls++ + var actual cty.Value + if !req.Attrs.IsNull() { + mp := req.Attrs.AsValueMap() + actual = cty.ObjectVal(map[string]cty.Value{ + "value": mp["value"], + "sensitive_value": mp["sensitive_value"], + }) + } + + if diff := cmp.Diff(actual, expected[req.Target], cmp.Comparer(cty.Value.RawEquals)); diff != "" { + t.Errorf("Unexpected diff (-got +want):\n%s", diff) + } + + if diff := cmp.Diff(req.Meta, &proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: "test", + Operation: proto.Operation_DELETE, + }, protocmp.Transform()); diff != "" { + t.Errorf("Invalid resource metadata: %s", diff) + } + + // Both resources have prior state, so PriorAttrs should be non-null. + if req.PriorAttrs.IsNull() { + t.Errorf("Expected non-null PriorAttrs for %s, got null", req.Target) + return policy.EvaluationResponse{} + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + }, + }, + { + name: "parent resource policy succeeds, child module resource policy fails", + expectCalls: 2, + mainConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + variable "input" { + type = string + default = "foo" + } + + resource "test_resource" "test" { + sensitive_value = "foo" + } + + module "child" { + source = "./child" + } + `, + childConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + resource "test_instance" "test" { + value = "forbidden_value" + } + `, + policyConfig: ` + resource_policy "test_resource" "parent_policy" { + enforce { + condition = attrs.sensitive_value == "foo" + } + } + + resource_policy "test_instance" "child_policy" { + enforce { + condition = attrs.value != "forbidden_value" + } + } + `, + prepareExpectations: func(t *testing.T, data *data) { + data.policy.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + data.policyEvalCalls++ + if diff := cmp.Diff(req.Meta, &proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: "test", + Operation: proto.Operation_CREATE, + }, protocmp.Transform()); diff != "" { + t.Errorf("Invalid resource metadata: %s", diff) + } + + if !req.PriorAttrs.IsNull() { + t.Errorf("Expected null PriorAttrs for newly created %s, got non-null", req.Target) + return policy.EvaluationResponse{} + } + + // Child module resource policy fails + if req.Target == "test_instance" { + return policy.EvaluationResponse{ + Overall: policy.DenyResult, + Enforcements: []policy.EnforcementResult{}, + Diagnostics: policy.DiagsFromProto([]*proto.Diagnostic{ + { + Severity: proto.Severity_ERROR, + Summary: "Child module policy violation", + Detail: "Resource test_instance.test violates policy: forbidden value detected", + Result: &proto.DiagnosticResult{ + Result: proto.EvaluateResult_DENY_EVALUATE_RESULT, + }, + Subject: &proto.Range{ + Filename: "child_policy.tfpolicy.hcl", + Start: &proto.Position{ + Line: 1, + Column: 1, + }, + End: &proto.Position{ + Line: 4, + Column: 10, + }, + }, + }, + }, nil), + } + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + }, + assertPolicyResults: func(t *testing.T, data *data) { + tfdiags.AssertDiagnosticCount(t, data.diags, 1) + var exp tfdiags.Diagnostics + // We want to test that the diagnostic subject is set to the terraform file, + // with an internal extra data for the policy file. + // This allows us to display both source information in the diagnostic. + policyClientDiag := data.diags[0] + policyExtra, ok := data.diags[0].ExtraInfo().(*policy.PolicyExtra) + if !ok { + t.Fatalf("Expected diagnostic extra info to be a *policy.PolicyExtra, got %T", policyClientDiag.ExtraInfo()) + } + tfSubject := policyClientDiag.Source().Subject.ToHCL().Ptr() + if filepath.Ext(tfSubject.Filename) != ".tf" { + t.Fatalf("Expected diagnostic subject filename to end with .tf, got %q", tfSubject.Filename) + } + if !strings.HasSuffix(policyExtra.Range.Subject.Filename, ".tfpolicy.hcl") { + t.Fatalf("Expected policy diagnostic subject filename to end with .tfpolicy.hcl, got %q", policyExtra.Range.Subject.Filename) + } + + exp = exp.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Child module policy violation", + Detail: "Resource test_instance.test violates policy: forbidden value detected", + Subject: tfSubject, + }) + tfdiags.AssertDiagnosticsMatch(t, data.diags, exp) + + // Check that parent resource was planned successfully but child resource was not + resourceChanges := data.plan.Changes.Resources + var parentFound, childFound bool + for _, change := range resourceChanges { + if change.Addr.String() == "test_resource.test" { + parentFound = true + } + if change.Addr.String() == "module.child.test_instance.test" { + childFound = true + } + } + + if !parentFound { + t.Error("Expected parent resource test_resource.test to be planned") + } + if !childFound { + t.Error("Expected child resource module.child.test_instance.test to be planned due to policy failure") + } + }, + }, + { + name: "destroy plan: policy is evaluated with null attrs", + expectCalls: 1, + mainConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + resource "test_resource" "test" { + sensitive_value = "foo" + } + `, + childConfig: "", + policyConfig: ` + resource_policy "test_resource" "policy_name" { + enforce { + condition = true + } + } + `, + planMode: plans.DestroyMode, + state: states.BuildState(func(ss *states.SyncState) { + ss.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_resource.test"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar","type":"test_resource","sensitive_value":"foo"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + }), + prepareExpectations: func(t *testing.T, data *data) { + // EvalPolicy should be called during the actual destroy plan with null attrs + var called int + data.policy.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + data.policyEvalCalls++ + called++ + if diff := cmp.Diff(req.Meta, &proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: "test", + Operation: proto.Operation_DELETE, + }, protocmp.Transform()); diff != "" { + t.Errorf("Invalid resource metadata: %s", diff) + } + + if !req.Attrs.IsNull() { + t.Errorf("Expected null attrs for destroy evaluation") + } + + if req.PriorAttrs.IsNull() { + t.Errorf("Expected non-null PriorAttrs for destroy evaluation") + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + t.Cleanup(func() { + if called != 1 { + t.Errorf("Expected EvalPolicy to be called once got %d", called) + } + }) + }, + assertPolicyResults: func(t *testing.T, d *data) { + if !d.policy.EvaluateCalled { + t.Error("Expected policyClient.Evaluate to be called for destroy plan") + } + tfdiags.AssertNoDiagnostics(t, d.diags) + + // Verify the plan contains a delete action + for _, rc := range d.plan.Changes.Resources { + if rc.Addr.String() == "test_resource.test" { + if rc.Action != plans.Delete { + t.Errorf("Expected delete action for test_resource.test, got %s", rc.Action) + } + return + } + } + t.Error("Expected test_resource.test in plan changes") + }, + }, + { + name: "destroy plan: policy denies destruction", + expectCalls: 1, + mainConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + resource "test_resource" "test" { + sensitive_value = "secret" + } + `, + childConfig: "", + policyConfig: ` + resource_policy "test_resource" "no_destroy" { + enforce { + condition = false + } + } + `, + planMode: plans.DestroyMode, + state: states.BuildState(func(ss *states.SyncState) { + ss.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_resource.test"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar","type":"test_resource","sensitive_value":"secret"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + }), + prepareExpectations: func(t *testing.T, data *data) { + data.policy.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + data.policyEvalCalls++ + if diff := cmp.Diff(req.Meta, proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: "test", + Operation: proto.Operation_DELETE, + }, protocmp.Transform()); diff != "" { + t.Errorf("Invalid resource metadata: %s", diff) + } + + if req.PriorAttrs.IsNull() { + t.Errorf("Expected non-null PriorAttrs for destroy evaluation") + return policy.EvaluationResponse{} + } + + return policy.EvaluationResponse{ + Overall: policy.DenyResult, + Enforcements: []policy.EnforcementResult{}, + Diagnostics: policy.DiagsFromProto([]*proto.Diagnostic{ + { + Severity: proto.Severity_ERROR, + Summary: "Destruction not allowed", + Detail: "Policy prevents destruction of test_resource.test", + Result: &proto.DiagnosticResult{ + Result: proto.EvaluateResult_DENY_EVALUATE_RESULT, + }, + Subject: &proto.Range{ + Filename: "no_destroy.tfpolicy.hcl", + Start: &proto.Position{ + Line: 1, + Column: 1, + }, + End: &proto.Position{ + Line: 4, + Column: 10, + }, + }, + }, + }, nil), + } + } + }, + assertPolicyResults: func(t *testing.T, d *data) { + if !d.policy.EvaluateCalled { + t.Error("Expected policyClient.Evaluate to be called for destroy plan") + } + tfdiags.AssertDiagnosticCount(t, d.diags, 1) + + var exp tfdiags.Diagnostics + policyClientDiag := d.diags[0] + tfSubject := policyClientDiag.Source().Subject.ToHCL().Ptr() + exp = exp.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Destruction not allowed", + Detail: "Policy prevents destruction of test_resource.test", + Subject: tfSubject, + }) + tfdiags.AssertDiagnosticsMatch(t, d.diags, exp) + }, + }, + { + name: "create resource with cbd. policy is evaluated with create operation", + expectCalls: 1, + mainConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + resource "test_resource" "test" { + sensitive_value = "after" + + lifecycle { + create_before_destroy = true + } + } + `, + childConfig: "", + policyConfig: ` + resource_policy "test_resource" "policy_name" { + enforce { + condition = true + } + } + `, + state: states.NewState(), + prepareExpectations: func(t *testing.T, data *data) { + var called int + data.policy.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + data.policyEvalCalls++ + called++ + if diff := cmp.Diff(req.Meta, &proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: "test", + Operation: proto.Operation_CREATE, + }, protocmp.Transform()); diff != "" { + t.Errorf("Invalid resource metadata: %s", diff) + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + t.Cleanup(func() { + if called != 1 { + t.Errorf("Expected EvalPolicy to be called once got %d", called) + } + }) + }, + }, + { + name: "update resource with cbd. policy is evaluated with update operation", + expectCalls: 1, + mainConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + resource "test_resource" "test" { + sensitive_value = "after" + + lifecycle { + create_before_destroy = true + } + } + `, + childConfig: "", + policyConfig: ` + resource_policy "test_resource" "policy_name" { + enforce { + condition = true + } + } + `, + state: states.BuildState(func(ss *states.SyncState) { + ss.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_resource.test"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar","type":"test_resource","sensitive_value":"secret"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + }), + prepareExpectations: func(t *testing.T, data *data) { + var called int + data.policy.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + data.policyEvalCalls++ + called++ + if diff := cmp.Diff(req.Meta, &proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: "test", + Operation: proto.Operation_UPDATE, + }, protocmp.Transform()); diff != "" { + t.Errorf("Invalid resource metadata: %s", diff) + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + t.Cleanup(func() { + if called != 1 { + t.Errorf("Expected EvalPolicy to be called once got %d", called) + } + }) + }, + }, + { + name: "replace resource with cbd. policy is evaluated with update operation", + expectCalls: 1, + mainConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + resource "test_resource" "test" { + sensitive_value = "after" + + lifecycle { + create_before_destroy = true + } + } + `, + childConfig: "", + policyConfig: ` + resource_policy "test_resource" "policy_name" { + enforce { + condition = true + } + } + `, + state: states.BuildState(func(ss *states.SyncState) { + ss.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_resource.test"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar","type":"test_resource","sensitive_value":"secret"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + }), + forceReplace: []addrs.AbsResourceInstance{mustResourceInstanceAddr("test_resource.test")}, + prepareExpectations: func(t *testing.T, data *data) { + var called int + data.policy.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + data.policyEvalCalls++ + called++ + if diff := cmp.Diff(req.Meta, &proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: "test", + Operation: proto.Operation_UPDATE, + }, protocmp.Transform()); diff != "" { + t.Errorf("Invalid resource metadata: %s", diff) + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + t.Cleanup(func() { + if called != 1 { + t.Errorf("Expected EvalPolicy to be called once got %d", called) + } + }) + }, + }, + { + name: "normal plan: removed config should send null attrs to policy", + expectCalls: 1, + mainConfig: ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + `, + childConfig: "", + policyConfig: ` + resource_policy "test_resource" "no_destroy" { + enforce { + condition = false + } + } + `, + planMode: plans.NormalMode, + state: states.BuildState(func(ss *states.SyncState) { + ss.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_resource.test"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar","type":"test_resource","sensitive_value":"secret"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + }), + prepareExpectations: func(t *testing.T, data *data) { + data.policy.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + data.policyEvalCalls++ + if req.PriorAttrs.IsNull() { + t.Errorf("Expected non-null PriorAttrs for destroy evaluation") + return policy.EvaluationResponse{} + } + + return policy.EvaluationResponse{ + Overall: policy.DenyResult, + Enforcements: []policy.EnforcementResult{}, + Diagnostics: policy.DiagsFromProto([]*proto.Diagnostic{ + { + Severity: proto.Severity_ERROR, + Summary: "Destruction not allowed", + Detail: "Policy prevents destruction of test_resource.test", + Result: &proto.DiagnosticResult{ + Result: proto.EvaluateResult_DENY_EVALUATE_RESULT, + }, + Subject: &proto.Range{ + Filename: "no_destroy.tfpolicy.hcl", + Start: &proto.Position{ + Line: 1, + Column: 1, + }, + End: &proto.Position{ + Line: 4, + Column: 10, + }, + }, + }, + }, nil), + } + } + }, + assertPolicyResults: func(t *testing.T, d *data) { + if !d.policy.EvaluateCalled { + t.Error("Expected policyClient.Evaluate to be called for destroy plan") + } + tfdiags.AssertDiagnosticCount(t, d.diags, 1) + + var exp tfdiags.Diagnostics + exp = exp.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Destruction not allowed", + Detail: "Policy prevents destruction of test_resource.test", + }) + tfdiags.AssertDiagnosticsMatch(t, d.diags, exp) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + configFiles := map[string]string{"main.tf": tc.mainConfig} + if tc.childConfig != "" { + configFiles["child/child.tf"] = tc.childConfig + } + if tc.policyConfig != "" { + configFiles["main.tfpolicy.hcl"] = tc.policyConfig + } + + mod := testModuleInline(t, configFiles) + providerAddr := addrs.NewDefaultProvider("test") + provider := testProvider("test") + state := states.NewState() + if tc.state != nil { + state = tc.state + } + + // mock expectations + policyClient := policy.NewTestMockClient(t) + data := &data{ + config: mod, + state: state, + policy: policyClient, + } + planMode := tc.planMode + if planMode == 0 { + planMode = plans.NormalMode + } + + if tc.prepareExpectations != nil { + tc.prepareExpectations(t, data) + } + + ctx, diags := NewContext(&ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + providerAddr: testProviderFuncFixed(provider), + }, + Parallelism: 1, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + plan, diags := ctx.Plan(mod, state, &PlanOpts{ + Mode: planMode, + SetVariables: testInputValuesUnset(mod.Module.Variables), + PolicyClient: policyClient, + DeferralAllowed: tc.deferralAllowed, + ForceReplace: tc.forceReplace, + }) + // The plan itself should not have diagnostics. Policy diagnostics are propagated via + // the PolicyResults object. + tfdiags.AssertNoDiagnostics(t, diags) + + data.plan = plan + + if data.policyEvalCalls != tc.expectCalls { + t.Fatalf("expected %d resource policy evaluation call(s), got %d", tc.expectCalls, data.policyEvalCalls) + } + + for _, result := range plan.PolicyResults.Iter() { + data.diags = data.diags.Append(result.EvaluationResponse.Diagnostics.AsTerraformDiags()) + } + if tc.assertPolicyResults != nil { + tc.assertPolicyResults(t, data) + } else { + tfdiags.AssertNoDiagnostics(t, data.diags) + } + }) + } +} + +func TestContext2Plan_PolicyEvaluation_NoResourceRunsAfterPolicy(t *testing.T) { + // This verifies that no resource instance node is run after policy evaluation + mainConfig := ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + resource "test_instance" "test" { + count = 2 + value = tostring(count.index) + } + ` + + policyConfig := ` + resource_policy "test_instance" "policy_name" { + enforce { + condition = true + } + } + ` + + mod := testModuleInline(t, map[string]string{ + "main.tf": mainConfig, + "main.tfpolicy.hcl": policyConfig, + }) + + providerAddr := addrs.NewDefaultProvider("test") + provider := testProvider("test") + + var policyRan atomic.Bool + var planCalls atomic.Int32 + + provider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + callNum := planCalls.Add(1) + if callNum == 2 { + time.Sleep(150 * time.Millisecond) + } + + if policyRan.Load() { + t.Fatalf("resource plan for %s ran after policy evaluation", req.TypeName) + } + + resp.PlannedState = req.ProposedNewState + return resp + } + + policyClient := policy.NewTestMockClient(t) + policyClient.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + policyRan.Store(true) + + if diff := cmp.Diff(req.Meta, &proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: "test", + Operation: proto.Operation_CREATE, + }, protocmp.Transform()); diff != "" { + t.Errorf("Invalid resource metadata: %s", diff) + } + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + ctx, diags := NewContext(&ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + providerAddr: testProviderFuncFixed(provider), + }, + Parallelism: 4, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + plan, diags := ctx.Plan(mod, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: testInputValuesUnset(mod.Module.Variables), + PolicyClient: policyClient, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + if !policyClient.EvaluateCalled { + t.Fatal("expected policy evaluation to be called during plan") + } + + if len(plan.Changes.Resources) != 2 { + t.Fatalf("expected 2 planned resource changes, got %d", len(plan.Changes.Resources)) + } + + var policyDiags tfdiags.Diagnostics + for _, result := range plan.PolicyResults.Iter() { + policyDiags = policyDiags.Append(result.EvaluationResponse.Diagnostics.AsTerraformDiags()) + } + tfdiags.AssertNoDiagnostics(t, policyDiags) +} + +func TestContext2Plan_PolicyEvaluation_ImportBlock(t *testing.T) { + mod := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_resource" "a" { + id = "importable" +} + +import { + to = test_resource.a + id = "importable" +} +`, + "main.tfpolicy.hcl": ` +resource_policy "test_resource" "policy_name" { + enforce { + condition = true + } +} +`, + }) + + providerAddr := addrs.NewDefaultProvider("test") + provider := testProvider("test") + provider.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }) + provider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + } + } + provider.ImportResourceStateFn = func(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + return providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_resource", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("importable"), + }), + }, + }, + } + } + + policyClient := policy.NewTestMockClient(t) + policyClient.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + t.Fatalf("expected policy evaluation to be skipped for import block planning, got request for %s", req.Target) + return policy.EvaluationResponse{} + } + + ctx, diags := NewContext(&ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + providerAddr: testProviderFuncFixed(provider), + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + plan, diags := ctx.Plan(mod, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: testInputValuesUnset(mod.Module.Variables), + PolicyClient: policyClient, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + if policyClient.EvaluateCalled { + t.Fatal("expected policy evaluation not to be called for import block planning") + } + + var policyDiags tfdiags.Diagnostics + for _, result := range plan.PolicyResults.Iter() { + policyDiags = policyDiags.Append(result.EvaluationResponse.Diagnostics.AsTerraformDiags()) + } + tfdiags.AssertNoDiagnostics(t, policyDiags) +} + +func TestContext2Plan_PolicyCallback(t *testing.T) { + // This test verifies that the GetResources callback provided during policy + // evaluation works correctly: matching all resources, filtering by + // attributes, returning nothing for non-matching filters, and returning + // nothing for non-existent resource types. + mainConfig := ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } + } + + resource "test_instance" "foo" { + ami = "bar" + } + + resource "test_instance" "baz" { + ami = "qux" + depends_on = [test_instance.foo] + } + + resource "test_instance" "boop" { + ami = "booper" + depends_on = [test_instance.baz] + } + ` + + policyConfig := ` + resource_policy "test_instance" "policy_name" { + enforce { + condition = core::getresources("some_resource_type", {})[0].value != null + } + } + ` + + mod := testModuleInline(t, map[string]string{ + "main.tf": mainConfig, + "main.tfpolicy.hcl": policyConfig, + }) + + providerAddr := addrs.NewDefaultProvider("test") + provider := testProvider("test") + provider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + } + } + + policyClient := policy.NewTestMockClient(t) + + type callbackResult struct { + matchAllResults []cty.Value + filteredResults []cty.Value + noMatchCount int + unknownTypeCount int + } + + var mu sync.Mutex + results := make(map[string]callbackResult) + + policyClient.EvaluateFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse { + cr := callbackResult{} + + if req.Callbacks.GetResources == nil { + t.Errorf("GetResources callback was nil") + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + // 1. Match all test_instance resources with null attrs (no filter). + all, err := req.Callbacks.GetResources("test_instance", cty.NullVal(cty.DynamicPseudoType)) + if err != nil { + t.Errorf("GetResources(test_instance, null): %v", err) + } else { + cr.matchAllResults = all + } + + // 2. Match resources with ami="bar" filter. + filtered, err := req.Callbacks.GetResources("test_instance", cty.ObjectVal(map[string]cty.Value{ + "ami": cty.StringVal("bar"), + })) + if err != nil { + t.Errorf("GetResources(test_instance, ami=bar): %v", err) + } else { + cr.filteredResults = filtered + } + + // 3. Match with an attribute filter that will never match any planned resource. + noMatch, err := req.Callbacks.GetResources("test_instance", cty.ObjectVal(map[string]cty.Value{ + "ami": cty.StringVal("nonexistent"), + })) + if err != nil { + t.Errorf("GetResources(test_instance, ami=nonexistent): %v", err) + } else { + cr.noMatchCount = len(noMatch) + } + + // 4. Query for a resource type that doesn't exist in the config. + unknown, err := req.Callbacks.GetResources("nonexistent_resource", cty.NullVal(cty.DynamicPseudoType)) + if err != nil { + t.Errorf("GetResources(nonexistent_resource): %v", err) + } else { + cr.unknownTypeCount = len(unknown) + } + + // Key by the ami attribute of the resource being evaluated. + ami := req.Attrs.GetAttr("ami").AsString() + mu.Lock() + results[ami] = cr + mu.Unlock() + + return policy.EvaluationResponse{Overall: policy.AllowResult} + } + + ctx, diags := NewContext(&ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + providerAddr: testProviderFuncFixed(provider), + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + plan, diags := ctx.Plan(mod, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: testInputValuesUnset(mod.Module.Variables), + PolicyClient: policyClient, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + var policyDiags tfdiags.Diagnostics + for _, result := range plan.PolicyResults.Iter() { + policyDiags = policyDiags.Append(result.EvaluationResponse.Diagnostics.AsTerraformDiags()) + } + tfdiags.AssertNoDiagnostics(t, policyDiags) + + // We expect exactly 3 evaluations (one per test_instance resource). + if len(results) != 3 { + t.Fatalf("expected 3 policy evaluations, got %d", len(results)) + } + + for ami, cr := range results { + expectedTotal := 3 + filteredCount := 1 + if len(cr.matchAllResults) != expectedTotal { + t.Errorf("evaluation[%s]: expected %d result for matchAll, got %d", ami, expectedTotal, len(cr.matchAllResults)) + } + + // Filtering by ami="nonexistent" should always return 0 for all evaluations. + if cr.noMatchCount != 0 { + t.Errorf("evaluation[%s]: expected 0 results for ami=nonexistent filter, got %d", ami, cr.noMatchCount) + } + + // Querying for a non-existent resource type should always return 0. + if cr.unknownTypeCount != 0 { + t.Errorf("evaluation[%s]: expected 0 results for nonexistent_resource, got %d", ami, cr.unknownTypeCount) + } + + // The filtered result should only match one resource "bar", except when evaluating "bar" itself. + if len(cr.filteredResults) != filteredCount { + t.Errorf("evaluation[%s]: expected filtered count %d, got %d", ami, filteredCount, len(cr.filteredResults)) + } + } +} diff --git a/internal/terraform/context_test.go b/internal/terraform/context_test.go index ef4276874c7b..2f8cfa4da565 100644 --- a/internal/terraform/context_test.go +++ b/internal/terraform/context_test.go @@ -637,6 +637,10 @@ func testProviderSchema(name string) *providers.GetProviderSchemaResponse { Sensitive: true, Optional: true, }, + "defer": { + Type: cty.Bool, + Optional: true, + }, "random": { Type: cty.String, Optional: true, diff --git a/internal/terraform/context_walk.go b/internal/terraform/context_walk.go index 886fab9f12a2..f41fb3c9fa84 100644 --- a/internal/terraform/context_walk.go +++ b/internal/terraform/context_walk.go @@ -12,12 +12,14 @@ import ( "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/deprecation" + "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/deferring" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/refactoring" "github.com/hashicorp/terraform/internal/resources/ephemeral" @@ -81,6 +83,11 @@ type graphWalkOpts struct { // Forget if set to true will cause the plan to forget all resources. This is // only allowd in the context of a destroy plan. Forget bool + + Locks map[addrs.Provider]*depsfile.ProviderLock + + PolicyClient policy.Client + PolicyResults *plans.PolicyResults } func (c *Context) walk(graph *Graph, operation walkOperation, opts *graphWalkOpts) (*ContextGraphWalker, tfdiags.Diagnostics) { @@ -188,6 +195,7 @@ func (c *Context) graphWalker(graph *Graph, operation walkOperation, opts *graph RefreshState: refreshState, Overrides: opts.Overrides, PrevRunState: prevRunState, + PolicyGraph: newPolicySubgraph(), Changes: changes.SyncWrapper(), NamedValues: namedvals.NewState(), EphemeralResources: ephemeral.NewResources(), @@ -202,6 +210,9 @@ func (c *Context) graphWalker(graph *Graph, operation walkOperation, opts *graph functionResults: opts.FunctionResults, Forget: opts.Forget, Actions: actions.NewActions(), + Locks: opts.Locks, + PolicyClient: opts.PolicyClient, + PolicyResults: opts.PolicyResults, Deprecations: deprecation.NewDeprecations(), } } diff --git a/internal/terraform/eval_context.go b/internal/terraform/eval_context.go index 7715fb2ad435..29ec84e31d4c 100644 --- a/internal/terraform/eval_context.go +++ b/internal/terraform/eval_context.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/deprecation" + "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" @@ -22,6 +23,7 @@ import ( "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/deferring" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/refactoring" @@ -180,6 +182,8 @@ type EvalContext interface { // meaningful comparison with RefreshState. PrevRunState() *states.SyncState + PolicyGraph() *policySubgraph + // InstanceExpander returns a helper object for tracking the expansion of // graph nodes during the plan phase in response to "count" and "for_each" // arguments. @@ -224,6 +228,14 @@ type EvalContext interface { // EvalContext. Actions() *actions.Actions + // ProviderLocks returns a read-only snapshot of provider locks (exact + // version per provider selected during init). + ProviderLocks() map[addrs.Provider]*depsfile.ProviderLock + + PolicyClient() policy.Client + PolicyResults() *plans.PolicyResults + + Config() *configs.Config // Deprecations returns the deprecations object that tracks meta-information // about deprecation, e.g. which module calls suppress deprecation warnings. Deprecations() *deprecation.Deprecations diff --git a/internal/terraform/eval_context_builtin.go b/internal/terraform/eval_context_builtin.go index 7e50ee8d2846..921f6eaa3ce1 100644 --- a/internal/terraform/eval_context_builtin.go +++ b/internal/terraform/eval_context_builtin.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/deprecation" + "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" @@ -26,6 +27,7 @@ import ( "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/deferring" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/refactoring" @@ -90,13 +92,37 @@ type BuiltinEvalContext struct { EphemeralResourcesValue *ephemeral.Resources RefreshStateValue *states.SyncState PrevRunStateValue *states.SyncState + PolicyGraphValue *policySubgraph InstanceExpanderValue *instances.Expander MoveResultsValue refactoring.MoveResults OverrideValues *mocking.Overrides ActionsValue *actions.Actions + LocksValue map[addrs.Provider]*depsfile.ProviderLock + PolicyClientValue policy.Client + PolicyResultsValue *plans.PolicyResults DeprecationsValue *deprecation.Deprecations } +func (ctx *BuiltinEvalContext) ProviderLocks() map[addrs.Provider]*depsfile.ProviderLock { + return ctx.LocksValue +} + +func (ctx *BuiltinEvalContext) PolicyClient() policy.Client { + return ctx.PolicyClientValue +} + +func (ctx *BuiltinEvalContext) PolicyResults() *plans.PolicyResults { + return ctx.PolicyResultsValue +} + +func (ctx *BuiltinEvalContext) PolicyGraph() *policySubgraph { + return ctx.PolicyGraphValue +} + +func (ctx *BuiltinEvalContext) Config() *configs.Config { + return ctx.Evaluator.Config +} + // BuiltinEvalContext implements EvalContext var _ EvalContext = (*BuiltinEvalContext)(nil) diff --git a/internal/terraform/eval_context_builtin_test.go b/internal/terraform/eval_context_builtin_test.go index cdfc74f9f407..1401c8a13331 100644 --- a/internal/terraform/eval_context_builtin_test.go +++ b/internal/terraform/eval_context_builtin_test.go @@ -135,6 +135,7 @@ func testBuiltinEvalContext(t *testing.T, op walkOperation, cfg *configs.Config, StateValue: state.SyncWrapper(), PrevRunStateValue: state.DeepCopy().SyncWrapper(), RefreshStateValue: state.DeepCopy().SyncWrapper(), + PolicyGraphValue: newPolicySubgraph(), NamedValuesValue: valState, ProviderLock: &sync.Mutex{}, ProviderCache: make(map[string]providers.Interface), diff --git a/internal/terraform/eval_context_mock.go b/internal/terraform/eval_context_mock.go index 9e3e7e1f0dec..38aabea9eee8 100644 --- a/internal/terraform/eval_context_mock.go +++ b/internal/terraform/eval_context_mock.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/deprecation" + "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" @@ -24,6 +25,7 @@ import ( "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/deferring" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/refactoring" @@ -154,6 +156,9 @@ type MockEvalContext struct { PrevRunStateCalled bool PrevRunStateState *states.SyncState + PolicyGraphCalled bool + PolicyGraphValue *policySubgraph + MoveResultsCalled bool MoveResultsResults refactoring.MoveResults @@ -172,8 +177,12 @@ type MockEvalContext struct { ActionsCalled bool ActionsState *actions.Actions - DeprecationCalled bool - DeprecationState *deprecation.Deprecations + ProviderLocksValue map[addrs.Provider]*depsfile.ProviderLock + PolicyClientValue policy.Client + PolicyResultsValue *plans.PolicyResults + ConfigValue *configs.Config + DeprecationCalled bool + DeprecationState *deprecation.Deprecations } // MockEvalContext implements EvalContext @@ -424,6 +433,11 @@ func (c *MockEvalContext) PrevRunState() *states.SyncState { return c.PrevRunStateState } +func (c *MockEvalContext) PolicyGraph() *policySubgraph { + c.PolicyGraphCalled = true + return c.PolicyGraphValue +} + func (c *MockEvalContext) MoveResults() refactoring.MoveResults { c.MoveResultsCalled = true return c.MoveResultsResults @@ -458,6 +472,22 @@ func (c *MockEvalContext) Actions() *actions.Actions { return c.ActionsState } +func (c *MockEvalContext) ProviderLocks() map[addrs.Provider]*depsfile.ProviderLock { + return c.ProviderLocksValue +} + +func (c *MockEvalContext) PolicyClient() policy.Client { + return c.PolicyClientValue +} + +func (c *MockEvalContext) Config() *configs.Config { + return c.ConfigValue +} + +func (c *MockEvalContext) PolicyResults() *plans.PolicyResults { + return c.PolicyResultsValue +} + func (c *MockEvalContext) Deprecations() *deprecation.Deprecations { c.DeprecationCalled = true if c.DeprecationState != nil { diff --git a/internal/terraform/graph_builder_apply.go b/internal/terraform/graph_builder_apply.go index 08828604f44b..7de53fe8aec8 100644 --- a/internal/terraform/graph_builder_apply.go +++ b/internal/terraform/graph_builder_apply.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -90,6 +91,9 @@ type ApplyGraphBuilder struct { // or test runtimes, where the root modules as Terraform sees them aren't // the actual root modules. AllowRootEphemeralOutputs bool + + // PolicyClient is the client for evaluating policies. + PolicyClient policy.Client } // See GraphBuilder @@ -142,6 +146,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { &ModuleVariableTransformer{ Config: b.Config, DestroyApply: b.Operation == walkDestroy, + PolicyClient: b.PolicyClient, }, &variableValidationTransformer{ operation: b.Operation, @@ -158,10 +163,11 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { // with dependency edges against the whole-resource nodes added by // ConfigTransformer above. &DiffTransformer{ - Concrete: concreteResourceInstance, - State: b.State, - Changes: b.Changes, - Config: b.Config, + Concrete: concreteResourceInstance, + State: b.State, + Changes: b.Changes, + Config: b.Config, + PolicyClient: b.PolicyClient, }, &ActionTriggerConfigTransformer{ @@ -212,7 +218,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { &AttachResourceConfigTransformer{Config: b.Config}, // add providers - transformProviders(concreteProvider, b.Config, b.ExternalProviderConfigs), + transformProviders(concreteProvider, b.Config, b.PolicyClient, b.ExternalProviderConfigs), // Remove modules no longer present in the config &RemovedModuleTransformer{Config: b.Config, State: b.State}, @@ -274,6 +280,9 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { // Close opened plugin connections &CloseProviderTransformer{}, + // Request policy evaluation for all resource instances. + &policyEvalTransformer{PolicyClient: b.PolicyClient}, + // close the root module &CloseRootModuleTransformer{}, diff --git a/internal/terraform/graph_builder_apply_test.go b/internal/terraform/graph_builder_apply_test.go index bc12a15496c5..d258b56cfbe7 100644 --- a/internal/terraform/graph_builder_apply_test.go +++ b/internal/terraform/graph_builder_apply_test.go @@ -12,7 +12,9 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" ) @@ -73,6 +75,62 @@ func TestApplyGraphBuilder(t *testing.T) { } } +func TestApplyGraphBuilder_PolicyClient(t *testing.T) { + changes := &plans.ChangesSrc{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("test_object.create"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + { + Addr: mustResourceInstanceAddr("test_object.other"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Update, + }, + }, + }, + } + + t.Run("with policy client", func(t *testing.T) { + b := &ApplyGraphBuilder{ + Config: testModule(t, "graph-builder-apply-basic"), + Changes: changes, + Plugins: simpleMockPluginLibrary(), + PolicyClient: policy.NewTestMockClient(t), + } + + g, diags := b.Build(addrs.RootModuleInstance) + if diags.HasErrors() { + t.Fatalf("err: %s", diags.Err()) + } + + policyNode := dag.SelectSeq[*nodePolicyEval](g.VerticesSeq()) + if nodes := len(policyNode.Collect()); nodes != 1 { + t.Fatalf("expected 1 policy evaluation node in apply graph with policy client, got %d", nodes) + } + }) + + t.Run("without policy client", func(t *testing.T) { + b := &ApplyGraphBuilder{ + Config: testModule(t, "graph-builder-apply-basic"), + Changes: changes, + Plugins: simpleMockPluginLibrary(), + } + + g, diags := b.Build(addrs.RootModuleInstance) + if diags.HasErrors() { + t.Fatalf("err: %s", diags.Err()) + } + + policyNode := dag.SelectSeq[*nodePolicyEval](g.VerticesSeq()) + if nodes := len(policyNode.Collect()); nodes != 0 { + t.Fatalf("expected 0 policy evaluation nodes in apply graph without policy client, got %d", nodes) + } + }) +} + // This tests the ordering of two resources where a non-CBD depends // on a CBD. GH-11349. func TestApplyGraphBuilder_depCbd(t *testing.T) { diff --git a/internal/terraform/graph_builder_eval.go b/internal/terraform/graph_builder_eval.go index ee8f8bcd285a..e28ab47ef006 100644 --- a/internal/terraform/graph_builder_eval.go +++ b/internal/terraform/graph_builder_eval.go @@ -91,7 +91,7 @@ func (b *EvalGraphBuilder) Steps() []GraphTransformer { // Attach the state &AttachStateTransformer{State: b.State}, - transformProviders(concreteProvider, b.Config, b.ExternalProviderConfigs), + transformProviders(concreteProvider, b.Config, nil, b.ExternalProviderConfigs), // Must attach schemas before ReferenceTransformer so that we can // analyze the configuration to find references. diff --git a/internal/terraform/graph_builder_plan.go b/internal/terraform/graph_builder_plan.go index 13d1c8e19e7a..9fac7ad75128 100644 --- a/internal/terraform/graph_builder_plan.go +++ b/internal/terraform/graph_builder_plan.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -117,6 +118,8 @@ type PlanGraphBuilder struct { // validation of the graph. SkipGraphValidation bool + PolicyClient policy.Client + // If true, the graph builder will generate a query plan instead of a // normal plan. This is used for the "terraform query" command. queryPlan bool @@ -203,6 +206,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { Config: b.Config, ValidateChecks: true, DestroyApply: false, // always false for planning + PolicyClient: b.PolicyClient, }, &variableValidationTransformer{ operation: b.Operation, @@ -263,7 +267,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { &AttachResourceConfigTransformer{Config: b.Config}, // add providers - transformProviders(b.ConcreteProvider, b.Config, b.ExternalProviderConfigs), + transformProviders(b.ConcreteProvider, b.Config, b.PolicyClient, b.ExternalProviderConfigs), // Remove modules no longer present in the config &RemovedModuleTransformer{Config: b.Config, State: b.State}, @@ -316,6 +320,17 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { // Close opened plugin connections &CloseProviderTransformer{}, + // Request policy evaluation for all resource instances. + &policyEvalTransformer{ + PolicyClient: func() policy.Client { + // Skip policy evaluation during predestroy refresh. + if b.preDestroyRefresh { + return nil + } + return b.PolicyClient + }(), + }, + // Close the root module &CloseRootModuleTransformer{}, diff --git a/internal/terraform/graph_builder_plan_test.go b/internal/terraform/graph_builder_plan_test.go index 588b6a41e31c..aca66bab2b93 100644 --- a/internal/terraform/graph_builder_plan_test.go +++ b/internal/terraform/graph_builder_plan_test.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" testing_provider "github.com/hashicorp/terraform/internal/providers/testing" ) @@ -93,6 +95,61 @@ openstack_floating_ip.random }) } +func TestPlanGraphBuilder_PolicyClient(t *testing.T) { + awsProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{Body: simpleTestSchema()}, + ResourceTypes: map[string]providers.Schema{ + "aws_security_group": {Body: simpleTestSchema()}, + "aws_instance": {Body: simpleTestSchema()}, + "aws_load_balancer": {Body: simpleTestSchema()}, + }, + }, + } + openstackProvider := mockProviderWithResourceTypeSchema("openstack_floating_ip", simpleTestSchema()) + plugins := newContextPlugins(map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): providers.FactoryFixed(awsProvider), + addrs.NewDefaultProvider("openstack"): providers.FactoryFixed(openstackProvider), + }, nil, nil) + + t.Run("with policy client", func(t *testing.T) { + b := &PlanGraphBuilder{ + Config: testModule(t, "graph-builder-plan-basic"), + Plugins: plugins, + PolicyClient: policy.NewTestMockClient(t), + Operation: walkPlan, + } + + g, diags := b.Build(addrs.RootModuleInstance) + if diags.HasErrors() { + t.Fatalf("err: %s", diags.Err()) + } + + policyNode := dag.SelectSeq[*nodePolicyEval](g.VerticesSeq()) + if nodes := len(policyNode.Collect()); nodes != 1 { + t.Fatalf("expected 1 policy evaluation node in plan graph with policy client, got %d", nodes) + } + }) + + t.Run("without policy client", func(t *testing.T) { + b := &PlanGraphBuilder{ + Config: testModule(t, "graph-builder-plan-basic"), + Plugins: plugins, + Operation: walkPlan, + } + + g, diags := b.Build(addrs.RootModuleInstance) + if diags.HasErrors() { + t.Fatalf("err: %s", diags.Err()) + } + + policyNode := dag.SelectSeq[*nodePolicyEval](g.VerticesSeq()) + if nodes := len(policyNode.Collect()); nodes != 0 { + t.Fatalf("expected 0 policy evaluation nodes in plan graph without policy client, got %d", nodes) + } + }) +} + func TestPlanGraphBuilder_dynamicBlock(t *testing.T) { provider := mockProviderWithResourceTypeSchema("test_thing", &configschema.Block{ Attributes: map[string]*configschema.Attribute{ diff --git a/internal/terraform/graph_policy.go b/internal/terraform/graph_policy.go new file mode 100644 index 000000000000..7824bcd06b1d --- /dev/null +++ b/internal/terraform/graph_policy.go @@ -0,0 +1,25 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "sync" +) + +type policySubgraph struct { + lock sync.Mutex + graph Graph +} + +func newPolicySubgraph() *policySubgraph { + var g Graph + return &policySubgraph{graph: g} +} + +func (ps *policySubgraph) Add(node *nodeResourcePolicy) { + ps.lock.Lock() + defer ps.lock.Unlock() + + ps.graph.Add(node) +} diff --git a/internal/terraform/graph_walk_context.go b/internal/terraform/graph_walk_context.go index 445398a31db1..010129cf6b76 100644 --- a/internal/terraform/graph_walk_context.go +++ b/internal/terraform/graph_walk_context.go @@ -15,12 +15,14 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/deprecation" + "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/deferring" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/refactoring" @@ -64,6 +66,12 @@ type ContextGraphWalker struct { // is in progress. NonFatalDiagnostics tfdiags.Diagnostics + Locks map[addrs.Provider]*depsfile.ProviderLock + + PolicyClient policy.Client + PolicyResults *plans.PolicyResults // Used to store policy evaluation results + PolicyGraph *policySubgraph // Used for writing resource policy evaluation nodes + once sync.Once contexts collections.Map[evalContextScope, *BuiltinEvalContext] contextLock sync.Mutex @@ -143,10 +151,14 @@ func (w *ContextGraphWalker) EvalContext() EvalContext { StateValue: w.State, RefreshStateValue: w.RefreshState, PrevRunStateValue: w.PrevRunState, + PolicyGraphValue: w.PolicyGraph, Evaluator: evaluator, OverrideValues: w.Overrides, forget: w.Forget, ActionsValue: w.Actions, + LocksValue: w.Locks, + PolicyClientValue: w.PolicyClient, + PolicyResultsValue: w.PolicyResults, DeprecationsValue: w.Deprecations, } diff --git a/internal/terraform/node_module_expand.go b/internal/terraform/node_module_expand.go index eb0099dc9c67..f3386e977271 100644 --- a/internal/terraform/node_module_expand.go +++ b/internal/terraform/node_module_expand.go @@ -10,7 +10,10 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/policy" + "github.com/hashicorp/terraform/internal/policy/proto" "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" ) type ConcreteModuleNodeFunc func(n *nodeExpandModule) dag.Vertex @@ -22,6 +25,10 @@ type nodeExpandModule struct { Addr addrs.Module Config *configs.Module ModuleCall *configs.ModuleCall + + // ModuleTree is the configuration bundle within the source that this module call + // refers to. + ModuleTree *configs.Config } var ( @@ -160,6 +167,10 @@ func (n *nodeExpandModule) Execute(globalCtx EvalContext, op walkOperation) (dia } } + if !diags.HasErrors() { + return diags.Append(n.EvalPolicy(globalCtx, op)) + } + return diags } @@ -295,5 +306,52 @@ func (n *nodeValidateModule) Execute(globalCtx EvalContext, op walkOperation) (d expander.SetModuleSingle(module, call) } + if !diags.HasErrors() { + return diags.Append(n.EvalPolicy(globalCtx, op)) + } + return diags } + +func (n *nodeExpandModule) EvalPolicy(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { + if ctx.PolicyClient() == nil { + log.Printf("[DEBUG] No policy client configured, skipping policy evaluation for %s", n.Addr) + return nil + } + + configBundle := n.ModuleTree + source := configBundle.SourceAddr.String() + + // Evaluate the module policy with just the metadata. Module variables cannot be sent here, + // because it is possible for them to depend on the module's output values, which are not available until after the module is expanded. + result := ctx.PolicyClient().EvaluateModule(ctx.StopCtx(), policy.EvaluationRequest[*proto.PolicyEvaluateModuleRequest_ModuleMetadata]{ + Attrs: cty.NilVal, + Target: source, + Meta: &proto.PolicyEvaluateModuleRequest_ModuleMetadata{ + Address: n.Addr.String(), + Version: func() string { + if configBundle.Version == nil { + return "" + } + return configBundle.Version.String() + }(), + }, + }) + + if n.ModuleCall.Config != nil { + ptr := n.ModuleCall.DeclRange.Ptr() + for idx, diag := range result.Diagnostics { + result.Diagnostics[idx] = diag.WithLocalRange(ptr) + } + for idx := range result.Enforcements { + result.Enforcements[idx].LocalRange = ptr + } + } + + // always add the result to the policy results + if ctx.PolicyResults() != nil { + ctx.PolicyResults().AddModule(n.Addr, result, n.ModuleCall) + } + + return nil +} diff --git a/internal/terraform/node_policy_eval.go b/internal/terraform/node_policy_eval.go new file mode 100644 index 000000000000..5ea2a87ad6dc --- /dev/null +++ b/internal/terraform/node_policy_eval.go @@ -0,0 +1,32 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// nodePolicyEval is a node that evaluates policy for all resource instances +// after they have been planned or applied, +// so that the complete resource graph state is available to the policy engine. +type nodePolicyEval struct{} + +var _ GraphNodeDynamicExpandable = (*nodePolicyEval)(nil) + +func (n *nodePolicyEval) Name() string { + return "(evaluate policies)" +} + +func (n *nodePolicyEval) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { + policyGraph := ctx.PolicyGraph() + if policyGraph == nil { + log.Printf("[DEBUG] policyGraph is nil") + return nil, nil + } + // ensure the graph has a single root + addRootNodeToGraph(&policyGraph.graph) + return &policyGraph.graph, nil +} diff --git a/internal/terraform/node_policy_resource.go b/internal/terraform/node_policy_resource.go new file mode 100644 index 000000000000..5cf1ba5447fc --- /dev/null +++ b/internal/terraform/node_policy_resource.go @@ -0,0 +1,110 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/policy/callback" + "github.com/hashicorp/terraform/internal/policy/proto" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +// nodeResourcePolicy is a node that evaluates a resource instance's policy. +type nodeResourcePolicy struct { + ResourceAddr addrs.AbsResourceInstance + ProviderAddr addrs.AbsProviderConfig + Before cty.Value + After cty.Value + Action plans.Action +} + +var _ GraphNodeExecutable = (*nodeResourcePolicy)(nil) + +func (n *nodeResourcePolicy) Name() string { + return n.ResourceAddr.String() + " (policy evaluation)" +} + +func (n *nodeResourcePolicy) Execute(ctx EvalContext, operation walkOperation) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + client := ctx.PolicyClient() + config := ctx.Config() + + if client == nil { + log.Printf("[DEBUG] No policy client configured, skipping policy evaluation") + return nil + } + if config == nil { + log.Printf("[DEBUG] No configuration available, skipping policy evaluation") + return nil + } + + providerAddr := n.ProviderAddr + provider, schema, err := getProvider(ctx, providerAddr) + if err != nil { + return diags.Append(err) + } + + modCfg := config.DescendantForInstance(n.ResourceAddr.Module) + if modCfg == nil { + return nil + } + + attrs, _ := n.After.UnmarkDeep() + priorAttrs, _ := n.Before.UnmarkDeep() + + var policyOperation proto.Operation + switch action := n.Action; action { + case plans.Create: + policyOperation = proto.Operation_CREATE + case plans.Delete: + policyOperation = proto.Operation_DELETE + case plans.Update, + plans.DeleteThenCreate, + plans.CreateThenDelete, + plans.CreateThenForget: + policyOperation = proto.Operation_UPDATE + default: + return nil + } + + meta := &proto.PolicyEvaluateResourceRequest_ResourceMetadata{ + ProviderType: providerAddr.Provider.Type, + Operation: policyOperation, + } + + providerRef := ProviderRef{ + addr: providerAddr, + resolved: true, + } + + metaVal, metaDiags := providerRef.getProviderMeta(ctx, n.ResourceAddr.Resource, modCfg.Module.ProviderMetas) + diags = diags.Append(metaDiags) + if diags.HasErrors() { + return diags + } + + callbacks := callback.Functions{ + GetResources: getResourcesForPolicyCallback(ctx, config), + GetDataSource: getDataSourceForPolicyCallback(ctx, provider, schema, metaVal), + } + + rscConfig := modCfg.Module.ResourceByAddr(n.ResourceAddr.Resource.Resource) + result := evaluatePolicies(ctx, operation, n.ResourceAddr, rscConfig, client, attrs, priorAttrs, meta, callbacks) + ctx.PolicyResults().AddResource(n.ResourceAddr, result, rscConfig) + return diags +} + +func policyNodeFromChange(change *plans.ResourceInstanceChange) *nodeResourcePolicy { + return &nodeResourcePolicy{ + ResourceAddr: change.Addr, + ProviderAddr: change.ProviderAddr, + Action: change.Action, + Before: change.Before, + After: change.After, + } +} diff --git a/internal/terraform/node_provider.go b/internal/terraform/node_provider.go index 9392eef33c26..e403503fb316 100644 --- a/internal/terraform/node_provider.go +++ b/internal/terraform/node_provider.go @@ -11,6 +11,8 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/policy" + "github.com/hashicorp/terraform/internal/policy/proto" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -40,18 +42,18 @@ func (n *NodeApplyableProvider) Execute(ctx EvalContext, op walkOperation) (diag switch op { case walkValidate: log.Printf("[TRACE] NodeApplyableProvider: validating configuration for %s", n.Addr) - return diags.Append(n.ValidateProvider(ctx, provider)) + return diags.Append(n.ValidateProvider(ctx, op, provider)) case walkPlan, walkPlanDestroy, walkApply, walkDestroy: log.Printf("[TRACE] NodeApplyableProvider: configuring %s", n.Addr) - return diags.Append(n.ConfigureProvider(ctx, provider, false)) + return diags.Append(n.ConfigureProvider(ctx, op, provider, false)) case walkImport: log.Printf("[TRACE] NodeApplyableProvider: configuring %s (requiring that configuration is wholly known)", n.Addr) - return diags.Append(n.ConfigureProvider(ctx, provider, true)) + return diags.Append(n.ConfigureProvider(ctx, op, provider, true)) } return diags } -func (n *NodeApplyableProvider) ValidateProvider(ctx EvalContext, provider providers.Interface) (diags tfdiags.Diagnostics) { +func (n *NodeApplyableProvider) ValidateProvider(ctx EvalContext, op walkOperation, provider providers.Interface) (diags tfdiags.Diagnostics) { configBody := buildProviderConfig(ctx, n.Addr, n.ProviderConfig()) @@ -107,7 +109,7 @@ func (n *NodeApplyableProvider) ValidateProvider(ctx EvalContext, provider provi // ConfigureProvider configures a provider that is already initialized and retrieved. // If verifyConfigIsKnown is true, ConfigureProvider will return an error if the // provider configVal is not wholly known and is meant only for use during import. -func (n *NodeApplyableProvider) ConfigureProvider(ctx EvalContext, provider providers.Interface, verifyConfigIsKnown bool) (diags tfdiags.Diagnostics) { +func (n *NodeApplyableProvider) ConfigureProvider(ctx EvalContext, op walkOperation, provider providers.Interface, verifyConfigIsKnown bool) (diags tfdiags.Diagnostics) { config := n.ProviderConfig() configBody := buildProviderConfig(ctx, n.Addr, config) @@ -155,12 +157,12 @@ func (n *NodeApplyableProvider) ConfigureProvider(ctx EvalContext, provider prov // If our config value contains any marked values, ensure those are // stripped out before sending this to the provider - unmarkedConfigVal, _ := configVal.UnmarkDeep() + n.cachedUnmarkedConfigValue, _ = configVal.UnmarkDeep() // Allow the provider to validate and insert any defaults into the full // configuration. req := providers.ValidateProviderConfigRequest{ - Config: unmarkedConfigVal, + Config: n.cachedUnmarkedConfigValue, } // ValidateProviderConfig is only used for validation. We are intentionally @@ -185,11 +187,11 @@ func (n *NodeApplyableProvider) ConfigureProvider(ctx EvalContext, provider prov // If the provider returns something different, log a warning to help // indicate to provider developers that the value is not used. preparedCfg := validateResp.PreparedConfig - if preparedCfg != cty.NilVal && !preparedCfg.IsNull() && !preparedCfg.RawEquals(unmarkedConfigVal) { + if preparedCfg != cty.NilVal && !preparedCfg.IsNull() && !preparedCfg.RawEquals(n.cachedUnmarkedConfigValue) { log.Printf("[WARN] ValidateProviderConfig from %q changed the config value, but that value is unused", n.Addr) } - configDiags := ctx.ConfigureProvider(n.Addr, unmarkedConfigVal) + configDiags := ctx.ConfigureProvider(n.Addr, n.cachedUnmarkedConfigValue) diags = diags.Append(configDiags.InConfigBody(configBody, n.Addr.String())) if diags.HasErrors() && config == nil { // If there isn't an explicit "provider" block in the configuration, @@ -201,9 +203,67 @@ func (n *NodeApplyableProvider) ConfigureProvider(ctx EvalContext, provider prov fmt.Sprintf(providerConfigErr, n.Addr.Provider), )) } + + // Post-provider config policy evaluation + policyDiags := n.EvalPolicy(ctx, op, n.cachedUnmarkedConfigValue) + diags = diags.Append(policyDiags) + if policyDiags.HasErrors() { + return diags + } + return diags } +func (n *NodeApplyableProvider) EvalPolicy(ctx EvalContext, op walkOperation, attrs cty.Value) tfdiags.Diagnostics { + if ctx.PolicyClient() == nil { + log.Printf("[DEBUG] No policy client configured, skipping policy evaluation for %s", n.Addr) + return nil + } + result := ctx.PolicyClient().EvaluateProvider(ctx.StopCtx(), policy.EvaluationRequest[*proto.PolicyEvaluateProviderRequest_ProviderMetadata]{ + Target: n.Addr.Provider.Type, + Attrs: attrs, + Meta: &proto.PolicyEvaluateProviderRequest_ProviderMetadata{ + Name: n.Addr.Provider.Type, + Alias: n.Addr.Alias, + Namespace: n.Addr.Provider.Namespace, + Source: n.Addr.Provider.String(), + ModulePath: n.Addr.Module.String(), + Version: n.providerVersion(ctx), + }, + }) + + // if this was an "implicit provider", and we have no configuration + // for it, There's going to be no source information for these errors. + if n.Config != nil { + ptr := n.Config.DeclRange.Ptr() + for idx, diag := range result.Diagnostics { + result.Diagnostics[idx] = diag.WithLocalRange(ptr) + } + for idx := range result.Enforcements { + result.Enforcements[idx].LocalRange = ptr + } + } + + // always add the result to the policy results + if ctx.PolicyResults() != nil { + ctx.PolicyResults().AddProvider(n.Addr, result, n.Config) + } + + return nil +} + +// providerVersion returns the exact locked version for this provider from the +// dependency lock file (e.g. "5.31.0"). Returns an empty string if no lock +// file entry is available for this provider. +func (n *NodeApplyableProvider) providerVersion(ctx EvalContext) string { + if providerLocks := ctx.ProviderLocks(); providerLocks != nil { + if lock := providerLocks[n.Addr.Provider]; lock != nil { + return lock.Version().String() + } + } + return "" +} + // nodeExternalProvider is used instead of [NodeApplyableProvider] when an // already-configured provider instance has been provided by an external caller, // and therefore we don't need to do anything to get the provider ready to diff --git a/internal/terraform/node_provider_abstract.go b/internal/terraform/node_provider_abstract.go index 06064bb05837..cc68ab8ed8f0 100644 --- a/internal/terraform/node_provider_abstract.go +++ b/internal/terraform/node_provider_abstract.go @@ -4,6 +4,8 @@ package terraform import ( + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -26,6 +28,10 @@ type NodeAbstractProvider struct { Config *configs.Provider Schema *configschema.Block + + // cachedUnmarkedConfigValue is set when the provider is configured, so + // that other nodes don't need to recalculate the config value. + cachedUnmarkedConfigValue cty.Value } var ( diff --git a/internal/terraform/node_provider_test.go b/internal/terraform/node_provider_test.go index ff152098c57c..08989950a2e1 100644 --- a/internal/terraform/node_provider_test.go +++ b/internal/terraform/node_provider_test.go @@ -14,7 +14,10 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/tfdiags" @@ -287,7 +290,7 @@ func TestNodeApplyableProvider_Validate(t *testing.T) { }, } - diags := node.ValidateProvider(ctx, provider) + diags := node.ValidateProvider(ctx, walkPlan, provider) if diags.HasErrors() { t.Errorf("unexpected error with valid config: %s", diags.Err()) } @@ -308,7 +311,7 @@ func TestNodeApplyableProvider_Validate(t *testing.T) { }, } - diags := node.ValidateProvider(ctx, provider) + diags := node.ValidateProvider(ctx, walkPlan, provider) if !diags.HasErrors() { t.Error("missing expected error with invalid config") } @@ -321,7 +324,7 @@ func TestNodeApplyableProvider_Validate(t *testing.T) { }, } - diags := node.ValidateProvider(ctx, provider) + diags := node.ValidateProvider(ctx, walkPlan, provider) if diags.HasErrors() { t.Errorf("unexpected error with empty config: %s", diags.Err()) } @@ -382,7 +385,7 @@ func TestNodeApplyableProvider_ConfigProvider(t *testing.T) { }, } - diags := node.ConfigureProvider(ctx, provider, false) + diags := node.ConfigureProvider(ctx, walkPlan, provider, false) if diags.HasErrors() { t.Errorf("unexpected error with valid config: %s", diags.Err()) } @@ -395,7 +398,7 @@ func TestNodeApplyableProvider_ConfigProvider(t *testing.T) { }, } - diags := node.ConfigureProvider(ctx, provider, false) + diags := node.ConfigureProvider(ctx, walkPlan, provider, false) if !diags.HasErrors() { t.Fatal("missing expected error with nil config") } @@ -416,7 +419,7 @@ func TestNodeApplyableProvider_ConfigProvider(t *testing.T) { }, } - diags := node.ConfigureProvider(ctx, provider, false) + diags := node.ConfigureProvider(ctx, walkPlan, provider, false) if !diags.HasErrors() { t.Fatal("missing expected error with invalid config") } @@ -432,7 +435,7 @@ func TestNodeApplyableProvider_ConfigProvider(t *testing.T) { }, } - diags := node.ConfigureProvider(ctx, requiredProvider, false) + diags := node.ConfigureProvider(ctx, walkPlan, requiredProvider, false) if !diags.HasErrors() { t.Fatal("missing expected error with nil config") } @@ -453,7 +456,7 @@ func TestNodeApplyableProvider_ConfigProvider(t *testing.T) { }, } - diags := node.ConfigureProvider(ctx, requiredProvider, false) + diags := node.ConfigureProvider(ctx, walkPlan, requiredProvider, false) if !diags.HasErrors() { t.Fatal("missing expected error with invalid config") } @@ -508,7 +511,7 @@ func TestNodeApplyableProvider_ConfigProvider_config_fn_err(t *testing.T) { }, } - diags := node.ConfigureProvider(ctx, provider, false) + diags := node.ConfigureProvider(ctx, walkPlan, provider, false) if diags.HasErrors() { t.Errorf("unexpected error with valid config: %s", diags.Err()) } @@ -521,7 +524,7 @@ func TestNodeApplyableProvider_ConfigProvider_config_fn_err(t *testing.T) { }, } - diags := node.ConfigureProvider(ctx, provider, false) + diags := node.ConfigureProvider(ctx, walkPlan, provider, false) if !diags.HasErrors() { t.Fatal("missing expected error with nil config") } @@ -542,7 +545,7 @@ func TestNodeApplyableProvider_ConfigProvider_config_fn_err(t *testing.T) { }, } - diags := node.ConfigureProvider(ctx, provider, false) + diags := node.ConfigureProvider(ctx, walkPlan, provider, false) if !diags.HasErrors() { t.Fatal("missing expected error with invalid config") } @@ -568,12 +571,207 @@ func TestGetSchemaError(t *testing.T) { }, } - diags := node.ConfigureProvider(ctx, provider, false) + diags := node.ConfigureProvider(ctx, walkPlan, provider, false) for _, d := range diags { desc := d.Description() if desc.Address != providerAddr.String() { t.Fatalf("missing provider address from diagnostics: %#v", desc) } } +} + +func TestNodeApplyableProvider_providerVersion(t *testing.T) { + providerAddr := addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("aws"), + } + + node := &NodeApplyableProvider{ + NodeAbstractProvider: &NodeAbstractProvider{ + Addr: providerAddr, + }, + } + + t.Run("with locked version", func(t *testing.T) { + lock := depsfile.NewProviderLock( + addrs.NewDefaultProvider("aws"), + providerreqs.MustParseVersion("5.31.0"), + nil, + nil, + ) + ctx := &MockEvalContext{ + ProviderLocksValue: map[addrs.Provider]*depsfile.ProviderLock{ + addrs.NewDefaultProvider("aws"): lock, + }, + } + + got := node.providerVersion(ctx) + if got != "5.31.0" { + t.Errorf("wrong version\ngot: %s\nwant: 5.31.0", got) + } + }) + + t.Run("no lock file", func(t *testing.T) { + ctx := &MockEvalContext{} + + got := node.providerVersion(ctx) + if got != "" { + t.Errorf("expected empty version, got: %s", got) + } + }) + + t.Run("lock file without this provider", func(t *testing.T) { + lock := depsfile.NewProviderLock( + addrs.NewDefaultProvider("google"), + providerreqs.MustParseVersion("4.0.0"), + nil, + nil, + ) + ctx := &MockEvalContext{ + ProviderLocksValue: map[addrs.Provider]*depsfile.ProviderLock{ + addrs.NewDefaultProvider("google"): lock, + }, + } + + got := node.providerVersion(ctx) + if got != "" { + t.Errorf("expected empty version, got: %s", got) + } + }) +} + +func TestNodeApplyableProvider_EvalPolicy_versionMeta(t *testing.T) { + providerAddr := addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("aws"), + } + + t.Run("version from lock file is passed in meta", func(t *testing.T) { + lock := depsfile.NewProviderLock( + addrs.NewDefaultProvider("aws"), + providerreqs.MustParseVersion("5.31.0"), + nil, + nil, + ) + + mockPolicy := &policy.MockClient{} + + ctx := &MockEvalContext{ + ProviderLocksValue: map[addrs.Provider]*depsfile.ProviderLock{ + addrs.NewDefaultProvider("aws"): lock, + }, + PolicyClientValue: mockPolicy, + } + + node := &NodeApplyableProvider{ + NodeAbstractProvider: &NodeAbstractProvider{ + Addr: providerAddr, + }, + } + + node.EvalPolicy(ctx, walkPlan, cty.EmptyObjectVal) + + if !mockPolicy.EvaluateProviderCalled { + t.Fatal("EvaluateProvider was not called") + } + + meta := mockPolicy.EvaluateProviderRequest.Meta + + gotVersion := meta.Version + if gotVersion != "5.31.0" { + t.Errorf("wrong version in meta\ngot: %s\nwant: 5.31.0", gotVersion) + } + }) + + t.Run("empty version when no lock file", func(t *testing.T) { + mockPolicy := &policy.MockClient{} + + ctx := &MockEvalContext{ + PolicyClientValue: mockPolicy, + } + + node := &NodeApplyableProvider{ + NodeAbstractProvider: &NodeAbstractProvider{ + Addr: providerAddr, + }, + } + + node.EvalPolicy(ctx, walkPlan, cty.EmptyObjectVal) + + if !mockPolicy.EvaluateProviderCalled { + t.Fatal("EvaluateProvider was not called") + } + + meta := mockPolicy.EvaluateProviderRequest.Meta + gotVersion := meta.Version + if gotVersion != "" { + t.Errorf("expected empty version in meta, got: %s", gotVersion) + } + }) + + t.Run("no policy client skips evaluation", func(t *testing.T) { + ctx := &MockEvalContext{} + + node := &NodeApplyableProvider{ + NodeAbstractProvider: &NodeAbstractProvider{ + Addr: providerAddr, + }, + } + + diags := node.EvalPolicy(ctx, walkPlan, cty.EmptyObjectVal) + if diags != nil { + t.Fatalf("unexpected diagnostics: %s", diags.Err()) + } + }) + + t.Run("meta contains all expected fields", func(t *testing.T) { + lock := depsfile.NewProviderLock( + addrs.NewDefaultProvider("aws"), + providerreqs.MustParseVersion("5.31.0"), + nil, + nil, + ) + mockPolicy := &policy.MockClient{} + + ctx := &MockEvalContext{ + ProviderLocksValue: map[addrs.Provider]*depsfile.ProviderLock{ + addrs.NewDefaultProvider("aws"): lock, + }, + PolicyClientValue: mockPolicy, + } + + node := &NodeApplyableProvider{ + NodeAbstractProvider: &NodeAbstractProvider{ + Addr: providerAddr, + }, + } + + node.EvalPolicy(ctx, walkPlan, cty.EmptyObjectVal) + + meta := mockPolicy.EvaluateProviderRequest.Meta + + checks := map[string]string{ + "name": meta.Name, + "alias": meta.Alias, + "namespace": meta.Namespace, + "source": meta.Source, + "module_path": meta.ModulePath, + "version": meta.Version, + } + expected := map[string]string{ + "name": "aws", + "alias": "", + "namespace": "hashicorp", + "source": "registry.terraform.io/hashicorp/aws", + "module_path": "", + "version": "5.31.0", + } + for field, got := range checks { + want := expected[field] + if got != want { + t.Errorf("wrong meta %q\ngot: %s\nwant: %s", field, got, want) + } + } + }) } diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 2ea1fc7c46e8..c2489037f5d0 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -204,6 +204,8 @@ func (n *NodeAbstractResourceInstance) preApplyHook(ctx EvalContext, change *pla if diags.HasErrors() { return diags } + + // TODO(sams): Implement pre-apply policy evaluation } return nil @@ -402,7 +404,7 @@ func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState return plan, deferred, diags.Append(err) } - metaConfigVal, metaDiags := n.providerMetas(ctx) + metaConfigVal, metaDiags := n.Provider().getProviderMeta(ctx, n.Addr.Resource, n.ProviderMetas) diags = diags.Append(metaDiags) if diags.HasErrors() { return plan, deferred, diags @@ -583,6 +585,10 @@ func (n *NodeAbstractResourceInstance) writeChange(ctx EvalContext, change *plan changes.AppendResourceInstanceChange(change) if deposedKey == states.NotDeposed { + // add the change to the policy graph if it's not a pre-destroy refresh + if policyGraph := ctx.PolicyGraph(); policyGraph != nil && !n.preDestroyRefresh { + policyGraph.Add(policyNodeFromChange(change)) + } log.Printf("[TRACE] writeChange: recorded %s change for %s", change.Action, n.Addr) } else { log.Printf("[TRACE] writeChange: recorded %s change for %s deposed object %s", change.Action, n.Addr, deposedKey) @@ -619,7 +625,7 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state return state, deferred, diags } - metaConfigVal, metaDiags := n.providerMetas(ctx) + metaConfigVal, metaDiags := n.Provider().getProviderMeta(ctx, n.Addr.Resource, n.ProviderMetas) diags = diags.Append(metaDiags) if diags.HasErrors() { return state, deferred, diags @@ -862,7 +868,7 @@ func (n *NodeAbstractResourceInstance) plan( return nil, nil, deferred, keyData, diags } - metaConfigVal, metaDiags := n.providerMetas(ctx) + metaConfigVal, metaDiags := n.Provider().getProviderMeta(ctx, n.Addr.Resource, n.ProviderMetas) diags = diags.Append(metaDiags) if diags.HasErrors() { return nil, nil, deferred, keyData, diags @@ -1629,7 +1635,7 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal return newVal, deferred, diags } - metaConfigVal, metaDiags := n.providerMetas(ctx) + metaConfigVal, metaDiags := n.Provider().getProviderMeta(ctx, n.Addr.Resource, n.ProviderMetas) diags = diags.Append(metaDiags) if diags.HasErrors() { return newVal, deferred, diags @@ -1760,37 +1766,6 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal return newVal, deferred, diags } -func (n *NodeAbstractResourceInstance) providerMetas(ctx EvalContext) (cty.Value, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - metaConfigVal := cty.NullVal(cty.DynamicPseudoType) - - _, providerSchema, err := getProvider(ctx, n.ResolvedProvider) - if err != nil { - return metaConfigVal, diags.Append(err) - } - if n.ProviderMetas != nil { - if m, ok := n.ProviderMetas[n.ResolvedProvider.Provider]; ok && m != nil { - // if the provider doesn't support this feature, throw an error - if providerSchema.ProviderMeta.Body == nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Provider %s doesn't support provider_meta", n.ResolvedProvider.Provider.String()), - Detail: fmt.Sprintf("The resource %s belongs to a provider that doesn't support provider_meta blocks", n.Addr.Resource), - Subject: &m.ProviderRange, - }) - } else { - var configDiags tfdiags.Diagnostics - metaConfigVal, _, configDiags = ctx.EvaluateBlock(m.Config, providerSchema.ProviderMeta.Body, nil, EvalDataForNoInstanceKey) - diags = diags.Append(configDiags) - var deprecationDiags tfdiags.Diagnostics - metaConfigVal, deprecationDiags = ctx.Deprecations().ValidateAndUnmarkConfig(metaConfigVal, providerSchema.ProviderMeta.Body, ctx.Path().Module()) - diags = diags.Append(deprecationDiags.InConfigBody(m.Config, n.Addr.String())) - } - } - } - return metaConfigVal, diags -} - // planDataSource deals with the main part of the data resource lifecycle: // either actually reading from the data source or generating a plan to do so. // @@ -2619,7 +2594,7 @@ func (n *NodeAbstractResourceInstance) apply( return state, diags } - metaConfigVal, metaDiags := n.providerMetas(ctx) + metaConfigVal, metaDiags := n.Provider().getProviderMeta(ctx, n.Addr.Resource, n.ProviderMetas) diags = diags.Append(metaDiags) if diags.HasErrors() { return state, diags diff --git a/internal/terraform/node_resource_apply_instance.go b/internal/terraform/node_resource_apply_instance.go index 047559b6ae6b..a60e164c8b12 100644 --- a/internal/terraform/node_resource_apply_instance.go +++ b/internal/terraform/node_resource_apply_instance.go @@ -265,6 +265,16 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext) diags = diags.Append(applyDiags) + if policyGraph := ctx.PolicyGraph(); policyGraph != nil { + policyGraph.Add(&nodeResourcePolicy{ + ResourceAddr: diffApply.Addr, + ProviderAddr: diffApply.ProviderAddr, + Before: diffApply.Before, + After: state.Value, + Action: diffApply.Action, + }) + } + // We clear the change out here so that future nodes don't see a change // that is already complete. err = n.writeChange(ctx, nil, "") diff --git a/internal/terraform/node_resource_destroy.go b/internal/terraform/node_resource_destroy.go index f8dc2d2c8627..dbf6b1cccfa1 100644 --- a/internal/terraform/node_resource_destroy.go +++ b/internal/terraform/node_resource_destroy.go @@ -150,6 +150,10 @@ func (n *NodeDestroyResourceInstance) managedResourceExecute(ctx EvalContext) (d return diags } + if policyGraph := ctx.PolicyGraph(); policyGraph != nil { + policyGraph.Add(policyNodeFromChange(changeApply)) + } + state, readDiags := n.readResourceInstanceState(ctx, addr) diags = diags.Append(readDiags) if diags.HasErrors() { diff --git a/internal/terraform/policy.go b/internal/terraform/policy.go new file mode 100644 index 000000000000..aca8cdb8eac4 --- /dev/null +++ b/internal/terraform/policy.go @@ -0,0 +1,122 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/policy" + "github.com/hashicorp/terraform/internal/policy/callback" + "github.com/hashicorp/terraform/internal/policy/proto" + "github.com/hashicorp/terraform/internal/providers" +) + +func evaluatePolicies(ctx EvalContext, walkOperation walkOperation, target addrs.AbsResourceInstance, config *configs.Resource, client policy.Client, attrs, priorAttrs cty.Value, meta *proto.PolicyEvaluateResourceRequest_ResourceMetadata, callbacks callback.Functions) policy.EvaluationResponse { + result := client.EvaluateResource(ctx.StopCtx(), policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]{ + Target: target.Resource.Resource.Type, + Attrs: attrs, + PriorAttrs: priorAttrs, + Meta: meta, + Callbacks: callbacks, + }) + + // orphaned resources do not have a config, so we can't provide source information + // for these errors. + if config != nil { + ptr := config.DeclRange.Ptr() + for idx, diag := range result.Diagnostics { + result.Diagnostics[idx] = diag.WithLocalRange(ptr) + } + for idx := range result.Enforcements { + result.Enforcements[idx].LocalRange = ptr + } + } + + return result +} + +func getResourcesForPolicyCallback(ctx EvalContext, config *configs.Config) func(target string, attrs cty.Value) ([]cty.Value, error) { + return func(target string, attrs cty.Value) ([]cty.Value, error) { + var found []cty.Value + config.DeepEach(func(c *configs.Config) { + for _, resource := range c.Module.ManagedResources { + if resource.Type != target { + continue + } + + resources := ctx.Changes().GetChangesForConfigResource(resource.Addr().InModule(c.Path)) + for _, change := range resources { + resource := change.After + if attrs.IsNull() { + // then match everything + found = append(found, resource) + continue + } + + value, matched := resource, true + for name, attr := range attrs.AsValueMap() { + if !value.Type().HasAttribute(name) { + matched = false + break + } + + equals := attr.Equals(value.GetAttr(name)) + if !equals.IsKnown() { + // We'll treat unknown values as matches, and they + // can be handled on the Terraform Policy side. + continue + } + + if equals.False() { + matched = false + break + } + } + + if matched { + value, _ = value.UnmarkDeep() + found = append(found, value) + } + + } + } + }) + return found, nil + } +} + +func getDataSourceForPolicyCallback(ctx EvalContext, provider providers.Interface, schema providers.GetProviderSchemaResponse, meta cty.Value) func(datasource string, attrs cty.Value) (cty.Value, error) { + return func(target string, attrs cty.Value) (cty.Value, error) { + if datasource, ok := schema.DataSources[target]; ok { + configVal, err := datasource.Body.CoerceValue(attrs) + if err != nil { + return cty.NilVal, fmt.Errorf("invalid attributes for %q: %w", target, err) + } + + validateResp := provider.ValidateDataResourceConfig(providers.ValidateDataResourceConfigRequest{ + TypeName: target, + Config: configVal, + }) + if err := validateResp.Diagnostics.Err(); err != nil { + return cty.NilVal, fmt.Errorf("failed to validate data source configuration: %s", err) + } + + readResp := provider.ReadDataSource(providers.ReadDataSourceRequest{ + TypeName: target, + Config: configVal, + ProviderMeta: meta, + }) + if err := readResp.Diagnostics.Err(); err != nil { + return cty.NilVal, fmt.Errorf("failed to read data source: %s", err) + } + + return readResp.State, nil + } + return cty.NilVal, fmt.Errorf("no data source found for %s", target) + } +} diff --git a/internal/terraform/transform_diff.go b/internal/terraform/transform_diff.go index bc17068f11cd..74edc8791990 100644 --- a/internal/terraform/transform_diff.go +++ b/internal/terraform/transform_diff.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -22,6 +23,8 @@ type DiffTransformer struct { State *states.State Changes *plans.ChangesSrc Config *configs.Config + + PolicyClient policy.Client } // return true if the given resource instance has either Preconditions or diff --git a/internal/terraform/transform_module_expansion.go b/internal/terraform/transform_module_expansion.go index b5b524130e0b..4edecba59bba 100644 --- a/internal/terraform/transform_module_expansion.go +++ b/internal/terraform/transform_module_expansion.go @@ -91,6 +91,7 @@ func (t *ModuleExpansionTransformer) transform(g *Graph, c *configs.Config, pare Addr: c.Path, Config: c.Module, ModuleCall: modCall, + ModuleTree: c, } var expander dag.Vertex = n if t.Concrete != nil { diff --git a/internal/terraform/transform_module_variable.go b/internal/terraform/transform_module_variable.go index fe7987467efb..b6a339aa51cc 100644 --- a/internal/terraform/transform_module_variable.go +++ b/internal/terraform/transform_module_variable.go @@ -9,6 +9,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/hcl/v2" @@ -39,6 +40,8 @@ type ModuleVariableTransformer struct { // DestroyApply must be set to true when applying a destroy operation and // false otherwise. DestroyApply bool + + PolicyClient policy.Client } func (t *ModuleVariableTransformer) Transform(g *Graph) error { diff --git a/internal/terraform/transform_policy_eval.go b/internal/terraform/transform_policy_eval.go new file mode 100644 index 000000000000..3f0f04c7a7f6 --- /dev/null +++ b/internal/terraform/transform_policy_eval.go @@ -0,0 +1,67 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/policy" +) + +// policyEvalTransformer is a transformer that adds the policy evaluation +// node to the graph. it also wires up the dependency edges to ensure that the +// node is executed after all resources have been planned or applied, and that +// providers are kept open until all policies have been evaluated. +type policyEvalTransformer struct { + PolicyClient policy.Client +} + +var _ GraphTransformer = (*policyEvalTransformer)(nil) + +func (t *policyEvalTransformer) Transform(g *Graph) error { + if t.PolicyClient == nil { + return nil + } + + // Collect all managed resource instance nodes and all provider closer + // nodes that are already in the graph. + var resourceNodes []dag.Vertex + var closeProviderNodes []dag.Vertex + + for v := range g.VerticesSeq() { + if ri, ok := v.(GraphNodeConfigResource); ok { + addr := ri.ResourceAddr() + if addr.Resource.Mode == addrs.ManagedResourceMode { + resourceNodes = append(resourceNodes, v) + } + } + + if _, ok := v.(GraphNodeCloseProvider); ok { + closeProviderNodes = append(closeProviderNodes, v) + } + } + + // If there are no managed resources at all, there is nothing to evaluate + // policy against. + if len(resourceNodes) == 0 { + return nil + } + + policyNode := &nodePolicyEval{} + g.Add(policyNode) + + // The policy node must execute after every managed resource instance node. + for _, rsNode := range resourceNodes { + g.Connect(dag.BasicEdge(policyNode, rsNode)) + } + + // We keep the provider open until after policy evaluation so that the + // policy engine callbacks can still use them. For example, policies may need to access data + // sources of a provider. + for _, providerNode := range closeProviderNodes { + g.Connect(dag.BasicEdge(providerNode, policyNode)) + } + + return nil +} diff --git a/internal/terraform/transform_provider.go b/internal/terraform/transform_provider.go index 2e89b35458fb..d23bcf6502ba 100644 --- a/internal/terraform/transform_provider.go +++ b/internal/terraform/transform_provider.go @@ -8,15 +8,17 @@ import ( "log" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" ) -func transformProviders(concrete ConcreteProviderNodeFunc, config *configs.Config, externalProviderConfigs map[addrs.RootProviderConfig]providers.Interface) GraphTransformer { +func transformProviders(concrete ConcreteProviderNodeFunc, config *configs.Config, policyClient policy.Client, externalProviderConfigs map[addrs.RootProviderConfig]providers.Interface) GraphTransformer { return GraphTransformMulti( // Add placeholder nodes for any externally-configured providers &externalProviderTransformer{ @@ -127,6 +129,38 @@ func (r ProviderRef) ForDisplay() string { return r.addr.Provider.ForDisplay() } +func (r ProviderRef) getProviderMeta(ctx EvalContext, resource addrs.ResourceInstance, metas map[addrs.Provider]*configs.ProviderMeta) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + metaConfigVal := cty.NullVal(cty.DynamicPseudoType) + + _, providerSchema, err := getProvider(ctx, r.addr) + if err != nil { + return metaConfigVal, diags.Append(err) + } + + if metas != nil { + if m, ok := metas[r.addr.Provider]; ok && m != nil { + // if the provider doesn't support this feature, throw an error + if providerSchema.ProviderMeta.Body == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Provider %s doesn't support provider_meta", r.addr.Provider.String()), + Detail: fmt.Sprintf("The resource %s belongs to a provider that doesn't support provider_meta blocks", resource.String()), + Subject: &m.ProviderRange, + }) + } else { + var configDiags tfdiags.Diagnostics + metaConfigVal, _, configDiags = ctx.EvaluateBlock(m.Config, providerSchema.ProviderMeta.Body, nil, EvalDataForNoInstanceKey) + diags = diags.Append(configDiags) + var deprecationDiags tfdiags.Diagnostics + metaConfigVal, deprecationDiags = ctx.Deprecations().ValidateAndUnmarkConfig(metaConfigVal, providerSchema.ProviderMeta.Body, ctx.Path().Module()) + diags = diags.Append(deprecationDiags.InConfigBody(m.Config, r.addr.String())) + } + } + } + return metaConfigVal, diags +} + // ProviderTransformer is a GraphTransformer that maps resources to providers // within the graph. This will error if there are any resources that don't map // to proper resources. diff --git a/internal/terraform/transform_provider_test.go b/internal/terraform/transform_provider_test.go index 2d7170844223..6cfe20dbbb1b 100644 --- a/internal/terraform/transform_provider_test.go +++ b/internal/terraform/transform_provider_test.go @@ -179,7 +179,7 @@ func TestMissingProviderTransformer_grandchildMissing(t *testing.T) { g := testProviderTransformerGraph(t, mod) { - transform := transformProviders(concrete, mod, nil) + transform := transformProviders(concrete, mod, nil, nil) if err := transform.Transform(g); err != nil { t.Fatalf("err: %s", err) } @@ -244,7 +244,7 @@ func TestProviderConfigTransformer_parentProviders(t *testing.T) { g := testProviderTransformerGraph(t, mod) { - tf := transformProviders(concrete, mod, nil) + tf := transformProviders(concrete, mod, nil, nil) if err := tf.Transform(g); err != nil { t.Fatalf("err: %s", err) } @@ -264,7 +264,7 @@ func TestProviderConfigTransformer_grandparentProviders(t *testing.T) { g := testProviderTransformerGraph(t, mod) { - tf := transformProviders(concrete, mod, nil) + tf := transformProviders(concrete, mod, nil, nil) if err := tf.Transform(g); err != nil { t.Fatalf("err: %s", err) } @@ -298,7 +298,7 @@ resource "test_object" "a" { g := testProviderTransformerGraph(t, mod) { - tf := transformProviders(concrete, mod, nil) + tf := transformProviders(concrete, mod, nil, nil) if err := tf.Transform(g); err != nil { t.Fatalf("err: %s", err) } @@ -376,7 +376,7 @@ resource "test_object" "a" { g := testProviderTransformerGraph(t, mod) { - tf := transformProviders(concrete, mod, nil) + tf := transformProviders(concrete, mod, nil, nil) if err := tf.Transform(g); err != nil { t.Fatalf("err: %s", err) } diff --git a/internal/tfdiags/diagnostic_base.go b/internal/tfdiags/diagnostic_base.go index 95b62237a3dd..5132f518603a 100644 --- a/internal/tfdiags/diagnostic_base.go +++ b/internal/tfdiags/diagnostic_base.go @@ -13,6 +13,7 @@ type diagnosticBase struct { summary string detail string address string + extra interface{} } var _ Diagnostic = &diagnosticBase{} @@ -43,5 +44,5 @@ func (d diagnosticBase) FromExpr() *FromExpr { } func (d diagnosticBase) ExtraInfo() interface{} { - return nil + return d.extra } diff --git a/internal/tfdiags/sourceless.go b/internal/tfdiags/sourceless.go index 1b25fa54abc2..a3957a76dd15 100644 --- a/internal/tfdiags/sourceless.go +++ b/internal/tfdiags/sourceless.go @@ -8,9 +8,14 @@ package tfdiags // caused by or relate to the environment where Terraform is running rather // than to the provided configuration. func Sourceless(severity Severity, summary, detail string) Diagnostic { + return SourcelessWithExtra(severity, summary, detail, nil) +} + +func SourcelessWithExtra(severity Severity, summary, detail string, extra any) Diagnostic { return diagnosticBase{ severity: severity, summary: summary, detail: detail, + extra: extra, } }