From 2d56f39dd91366533cdffe2d9cb725d622bf32f6 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 30 Oct 2025 17:31:19 -0400 Subject: [PATCH 01/16] initial implementation --- go.mod | 32 +-- go.sum | 66 +++---- internal/provider/action_local_command.go | 183 ++++++++++++++++++ .../provider/action_local_command_test.go | 47 +++++ internal/provider/provider.go | 9 +- .../testdata/TestLocalCommandAction/main.tf | 25 +++ .../scripts/example_script.sh | 4 + 7 files changed, 316 insertions(+), 50 deletions(-) create mode 100644 internal/provider/action_local_command.go create mode 100644 internal/provider/action_local_command_test.go create mode 100644 internal/provider/testdata/TestLocalCommandAction/main.tf create mode 100644 internal/provider/testdata/TestLocalCommandAction/scripts/example_script.sh diff --git a/go.mod b/go.mod index a69953da..37c2570f 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,14 @@ module github.com/terraform-providers/terraform-provider-local go 1.24.0 +replace github.com/hashicorp/terraform-plugin-testing => /Users/austin.valle/code/terraform-plugin-testing + require ( github.com/google/go-cmp v0.7.0 github.com/hashicorp/terraform-plugin-framework v1.16.1 github.com/hashicorp/terraform-plugin-framework-validators v0.19.0 github.com/hashicorp/terraform-plugin-go v0.29.0 + github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.13.3 ) @@ -28,35 +31,34 @@ require ( github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hc-install v0.9.2 // indirect - github.com/hashicorp/hcl/v2 v2.23.0 // indirect + github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-exec v0.23.0 // indirect - github.com/hashicorp/terraform-json v0.25.0 // indirect - github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect - github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 // indirect + github.com/hashicorp/terraform-exec v0.24.0 // indirect + github.com/hashicorp/terraform-json v0.27.2 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 // indirect github.com/hashicorp/terraform-registry-address v0.4.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect - github.com/kr/pretty v0.3.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.1.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/zclconf/go-cty v1.16.3 // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/mod v0.26.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/tools v0.35.0 // indirect + github.com/zclconf/go-cty v1.17.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.45.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/grpc v1.75.1 // indirect diff --git a/go.sum b/go.sum index 101bd83f..d1dbb316 100644 --- a/go.sum +++ b/go.sum @@ -75,14 +75,14 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= -github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= -github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= -github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= -github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= -github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= +github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE= +github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= +github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= +github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-framework v1.16.1 h1:1+zwFm3MEqd/0K3YBB2v9u9DtyYHyEuhVOfeIXbteWA= github.com/hashicorp/terraform-plugin-framework v1.16.1/go.mod h1:0xFOxLy5lRzDTayc4dzK/FakIgBhNf/lC4499R9cV4Y= github.com/hashicorp/terraform-plugin-framework-validators v0.19.0 h1:Zz3iGgzxe/1XBkooZCewS0nJAaCFPFPHdNJd8FgE4Ow= @@ -91,10 +91,8 @@ github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+ github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsore1ZaRWU9cnB6jFoBnIM= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0/go.mod h1:QYmYnLfsosrxjCnGY1p9c7Zj6n9thnEE+7RObeYs3fA= -github.com/hashicorp/terraform-plugin-testing v1.13.3 h1:QLi/khB8Z0a5L54AfPrHukFpnwsGL8cwwswj4RZduCo= -github.com/hashicorp/terraform-plugin-testing v1.13.3/go.mod h1:WHQ9FDdiLoneey2/QHpGM/6SAYf4A7AZazVg7230pLE= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1/go.mod h1:GQhpKVvvuwzD79e8/NZ+xzj+ZpWovdPAe8nfV/skwNU= github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -108,8 +106,8 @@ github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -127,8 +125,8 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -140,8 +138,8 @@ github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxu github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= @@ -160,8 +158,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= -github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= +github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -178,22 +176,22 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -206,21 +204,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= @@ -237,9 +235,9 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/provider/action_local_command.go b/internal/provider/action_local_command.go new file mode 100644 index 00000000..bdd56696 --- /dev/null +++ b/internal/provider/action_local_command.go @@ -0,0 +1,183 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "bytes" + "context" + "fmt" + "os/exec" + "runtime" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var ( + _ action.Action = (*localCommandAction)(nil) +) + +func NewLocalCommandAction() action.Action { + return &localCommandAction{} +} + +type localCommandAction struct{} + +func (a *localCommandAction) Metadata(ctx context.Context, req action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_command" +} + +func (a *localCommandAction) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "", // TODO: Describe action, mention that actions don't have output, this is meant to execute local commands, they can be non-idempotent as they are only executed during apply. + // If the external command is idempotent/you need the output, use data source (coming soon). + Attributes: map[string]schema.Attribute{ + "command": schema.StringAttribute{ + Description: "Executable name to be discovered on the PATH or absolute path to executable.", + Required: true, + }, + "arguments": schema.ListAttribute{ + Description: "Arguments to be passed to the given command.", + ElementType: types.StringType, + Optional: true, + }, + "stdin": schema.StringAttribute{ + Description: "Data to be passed to the given command's standard input.", + Optional: true, + }, + "working_directory": schema.StringAttribute{ + Description: "The directory where the command should be executed. Defaults to the current working directory.", + Optional: true, + }, + }, + } +} + +type localCommandActionModel struct { + Command types.String `tfsdk:"command"` + Arguments types.List `tfsdk:"arguments"` + Stdin types.String `tfsdk:"stdin"` + WorkingDirectory types.String `tfsdk:"working_directory"` +} + +func (a *localCommandAction) ModifyPlan(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + var command types.String + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("command"), &command)...) + if resp.Diagnostics.HasError() || command.IsUnknown() { + return + } + + resp.Diagnostics.Append(findCommand(command.ValueString())) +} + +func (a *localCommandAction) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + var config localCommandActionModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + // Prep the commmand + command := config.Command.ValueString() + resp.Diagnostics.Append(findCommand(command)) + if resp.Diagnostics.HasError() { + return + } + + arguments := make([]string, 0) + resp.Diagnostics.Append(config.Arguments.ElementsAs(ctx, &arguments, true)...) + if resp.Diagnostics.HasError() { + return + } + + cmd := exec.CommandContext(ctx, command, arguments...) + + cmd.Dir = config.WorkingDirectory.ValueString() + + if !config.Stdin.IsNull() { + cmd.Stdin = bytes.NewReader([]byte(config.Stdin.ValueString())) + } + + var stderr strings.Builder + cmd.Stderr = &stderr + + tflog.Trace(ctx, "Executing local command", map[string]interface{}{"command": cmd.String()}) + + // Run the command + stdout, err := cmd.Output() + stdoutStr := string(stdout) + stderrStr := stderr.String() + + if err != nil { + if len(stderrStr) > 0 { + resp.Diagnostics.AddAttributeError( + path.Root("command"), + "Command Execution Failed", + "The action received an unexpected error while attempting to execute the command."+ + "\n\n"+ + fmt.Sprintf("Commmand: %s\n", cmd.String())+ + fmt.Sprintf("Command Error: %s\n", stderrStr)+ + fmt.Sprintf("Error Message: %s", err), + ) + return + } + + resp.Diagnostics.Append(genericCommandDiag(cmd, err)) + return + } + + tflog.Trace(ctx, "Executed local command", map[string]interface{}{"command": cmd.String(), "stdout": stdoutStr, "stderr": stderrStr}) + + // Send the STDOUT to Terraform to display to the practitioner. The underlying action protocol supports streaming the + // STDOUT line-by-line in real-time, although each progress message gets a prefix per line, so it'd be difficult + // to read without batching lines together with an arbitrary time interval. (we can do this later if needed) + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("\n\n%s\n", stdoutStr), + }) +} + +func findCommand(command string) diag.Diagnostic { + if _, err := exec.LookPath(command); err != nil { + return diag.NewAttributeErrorDiagnostic( + path.Root("command"), + "Command Lookup Failed", + "The action received an unexpected error while attempting to find the command."+ + "\n\n"+ + "The command must be accessible according to the platform where Terraform is running."+ + "\n\n"+ + "If the expected command should be automatically found on the platform where Terraform is running, "+ + "ensure that the command is in an expected directory. On Unix-based platforms, these directories are "+ + "typically searched based on the '$PATH' environment variable. On Windows-based platforms, these directories "+ + "are typically searched based on the '%PATH%' environment variable."+ + "\n\n"+ + "If the expected command is relative to the Terraform configuration, it is recommended that the command name includes "+ + "the interpolated value of 'path.module' before the command name to ensure that it is compatible with varying module usage. For example: \"${path.module}/my-command\""+ + "\n\n"+ + "The command must also be executable according to the platform where Terraform is running. On Unix-based platforms, the file on the filesystem must have the executable bit set. "+ + "On Windows-based platforms, no action is typically necessary."+ + "\n\n"+ + fmt.Sprintf("Platform: %s\n", runtime.GOOS)+ + fmt.Sprintf("Command: %s\n", command)+ + fmt.Sprintf("Error Message: %s", err), + ) + } + + return nil +} + +func genericCommandDiag(cmd *exec.Cmd, err error) diag.Diagnostic { + return diag.NewAttributeErrorDiagnostic( + path.Root("command"), + "Command Execution Failed", + "The action received an unexpected error while attempting to execute the command."+ + "\n\n"+ + fmt.Sprintf("Commmand: %s\n", cmd.Path)+ + fmt.Sprintf("Error Message: %s", err), + ) +} diff --git a/internal/provider/action_local_command_test.go b/internal/provider/action_local_command_test.go new file mode 100644 index 00000000..ec4162b6 --- /dev/null +++ b/internal/provider/action_local_command_test.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/actioncheck" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestLocalCommandAction(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + testScriptsDir := filepath.Join(wd, "testdata", t.Name(), "scripts") + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "stdin": config.StringVariable("Austin"), + "working_directory": config.StringVariable(testScriptsDir), + }, + ConfigDirectory: config.TestNameDirectory(), + ActionChecks: []actioncheck.ActionCheck{ + actioncheck.ExpectProgressMessageContains("local_command", "Hello Austin!"), + }, + PostApplyFunc: func() { + fmt.Println("we're done!") + }, + }, + }, + }) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 907ce2f3..69294a52 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -12,6 +12,7 @@ import ( "encoding/base64" "encoding/hex" + "github.com/hashicorp/terraform-plugin-framework/action" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -20,7 +21,7 @@ import ( ) var ( - _ provider.ProviderWithFunctions = (*localProvider)(nil) + _ provider.Provider = (*localProvider)(nil) ) func New() provider.Provider { @@ -57,6 +58,12 @@ func (p *localProvider) Functions(ctx context.Context) []func() function.Functio } } +func (p *localProvider) Actions(ctx context.Context) []func() action.Action { + return []func() action.Action{ + NewLocalCommandAction, + } +} + func (p *localProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { resp.Schema = schema.Schema{} } diff --git a/internal/provider/testdata/TestLocalCommandAction/main.tf b/internal/provider/testdata/TestLocalCommandAction/main.tf new file mode 100644 index 00000000..e8bea22a --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction/main.tf @@ -0,0 +1,25 @@ +variable "stdin" { + type = string +} + +variable "working_directory" { + type = string +} + +resource "terraform_data" "test" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.local_command.bash_test] + } + } +} + +action "local_command" "bash_test" { + config { + command = "bash" + arguments = ["example_script.sh"] + stdin = var.stdin + working_directory = var.working_directory + } +} diff --git a/internal/provider/testdata/TestLocalCommandAction/scripts/example_script.sh b/internal/provider/testdata/TestLocalCommandAction/scripts/example_script.sh new file mode 100644 index 00000000..536090ae --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction/scripts/example_script.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +NAME=$( Date: Thu, 30 Oct 2025 17:49:07 -0400 Subject: [PATCH 02/16] update the config directory to work running manually --- .../testdata/TestLocalCommandAction/main.tf | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/internal/provider/testdata/TestLocalCommandAction/main.tf b/internal/provider/testdata/TestLocalCommandAction/main.tf index e8bea22a..9c372a5b 100644 --- a/internal/provider/testdata/TestLocalCommandAction/main.tf +++ b/internal/provider/testdata/TestLocalCommandAction/main.tf @@ -3,7 +3,8 @@ variable "stdin" { } variable "working_directory" { - type = string + type = string + default = "" } resource "terraform_data" "test" { @@ -17,9 +18,17 @@ resource "terraform_data" "test" { action "local_command" "bash_test" { config { - command = "bash" - arguments = ["example_script.sh"] - stdin = var.stdin - working_directory = var.working_directory + command = "bash" + arguments = ["example_script.sh"] + stdin = var.stdin + + # This configuration will get copied to a temporary location without the scripts folder, so for + # acceptance tests we pass the working directory from the Go test environment via a variable. + # If running manually, there is no need to provide the working_directory. + working_directory = ( + var.working_directory != "" ? + var.working_directory : + "${abspath(path.module)}/scripts" + ) } } From b70be81f8d6fbdba799fc11eae3dfc23521f6990 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 30 Oct 2025 17:50:57 -0400 Subject: [PATCH 03/16] add count --- internal/provider/action_local_command_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/provider/action_local_command_test.go b/internal/provider/action_local_command_test.go index ec4162b6..573d421a 100644 --- a/internal/provider/action_local_command_test.go +++ b/internal/provider/action_local_command_test.go @@ -36,6 +36,7 @@ func TestLocalCommandAction(t *testing.T) { }, ConfigDirectory: config.TestNameDirectory(), ActionChecks: []actioncheck.ActionCheck{ + actioncheck.ExpectProgressCount("local_command", 1), actioncheck.ExpectProgressMessageContains("local_command", "Hello Austin!"), }, PostApplyFunc: func() { From e9123aa30a5bc64ec10279dae2de3172f9c2d7eb Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 31 Oct 2025 15:21:06 -0400 Subject: [PATCH 04/16] add test --- .../provider/action_local_command_test.go | 35 ++++++++++++-- .../testdata/TestLocalCommandAction/main.tf | 34 ------------- .../TestLocalCommandAction_bash/main.tf | 48 +++++++++++++++++++ .../scripts/example_script.sh | 2 + 4 files changed, 80 insertions(+), 39 deletions(-) delete mode 100644 internal/provider/testdata/TestLocalCommandAction/main.tf create mode 100644 internal/provider/testdata/TestLocalCommandAction_bash/main.tf rename internal/provider/testdata/{TestLocalCommandAction => TestLocalCommandAction_bash}/scripts/example_script.sh (55%) diff --git a/internal/provider/action_local_command_test.go b/internal/provider/action_local_command_test.go index 573d421a..f52857f6 100644 --- a/internal/provider/action_local_command_test.go +++ b/internal/provider/action_local_command_test.go @@ -5,22 +5,34 @@ package provider import ( "fmt" + "math/rand" "os" "path/filepath" "testing" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-testing/actioncheck" "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func TestLocalCommandAction(t *testing.T) { +// This test calls the "bash" command and passes the STDIN and arguments to a bash script +// that prints to STDOUT and creates a file in the temporary test directory. +func TestLocalCommandAction_bash(t *testing.T) { wd, err := os.Getwd() if err != nil { t.Fatalf("Failed to get working directory: %v", err) } + testScriptsDir := filepath.Join(wd, "testdata", t.Name(), "scripts") + tempDir := t.TempDir() + stdin := "John" + randomNumber1 := rand.Intn(100) + randomNumber2 := rand.Intn(100) + randomNumber3 := rand.Intn(100) + + expectedFileContent := fmt.Sprintf("%s - args: %d %d %d\n", stdin, randomNumber1, randomNumber2, randomNumber3) resource.UnitTest(t, resource.TestCase{ // Actions are only available in 1.14 and later @@ -31,16 +43,29 @@ func TestLocalCommandAction(t *testing.T) { Steps: []resource.TestStep{ { ConfigVariables: config.Variables{ - "stdin": config.StringVariable("Austin"), - "working_directory": config.StringVariable(testScriptsDir), + "stdin": config.StringVariable(stdin), + "working_directory": config.StringVariable(tempDir), + "scripts_folder_path": config.StringVariable(testScriptsDir), + "arguments": config.ListVariable( + config.IntegerVariable(randomNumber1), + config.IntegerVariable(randomNumber2), + config.IntegerVariable(randomNumber3), + ), }, ConfigDirectory: config.TestNameDirectory(), ActionChecks: []actioncheck.ActionCheck{ actioncheck.ExpectProgressCount("local_command", 1), - actioncheck.ExpectProgressMessageContains("local_command", "Hello Austin!"), + actioncheck.ExpectProgressMessageContains("local_command", fmt.Sprintf("Hello %s!", stdin)), }, PostApplyFunc: func() { - fmt.Println("we're done!") + testFile, err := os.ReadFile(filepath.Join(tempDir, "test_file.txt")) + if err != nil { + t.Fatalf("error trying to read created test file: %s", err) + } + + if diff := cmp.Diff(expectedFileContent, string(testFile)); diff != "" { + t.Fatalf("unexpected file diff (-expected, +got): %s", diff) + } }, }, }, diff --git a/internal/provider/testdata/TestLocalCommandAction/main.tf b/internal/provider/testdata/TestLocalCommandAction/main.tf deleted file mode 100644 index 9c372a5b..00000000 --- a/internal/provider/testdata/TestLocalCommandAction/main.tf +++ /dev/null @@ -1,34 +0,0 @@ -variable "stdin" { - type = string -} - -variable "working_directory" { - type = string - default = "" -} - -resource "terraform_data" "test" { - lifecycle { - action_trigger { - events = [after_create] - actions = [action.local_command.bash_test] - } - } -} - -action "local_command" "bash_test" { - config { - command = "bash" - arguments = ["example_script.sh"] - stdin = var.stdin - - # This configuration will get copied to a temporary location without the scripts folder, so for - # acceptance tests we pass the working directory from the Go test environment via a variable. - # If running manually, there is no need to provide the working_directory. - working_directory = ( - var.working_directory != "" ? - var.working_directory : - "${abspath(path.module)}/scripts" - ) - } -} diff --git a/internal/provider/testdata/TestLocalCommandAction_bash/main.tf b/internal/provider/testdata/TestLocalCommandAction_bash/main.tf new file mode 100644 index 00000000..82a5df83 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_bash/main.tf @@ -0,0 +1,48 @@ +variable "stdin" { + type = string +} + +variable "arguments" { + type = list(string) + default = [] +} + +variable "working_directory" { + type = string + default = null +} + +variable "scripts_folder_path" { + type = string + default = null +} + +resource "terraform_data" "test" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.local_command.bash_test] + } + } +} + +locals { + test_script = ( + # This configuration will get copied to a temporary location without the scripts folder, so for + # acceptance tests we pass the folder path from the Go test environment via a variable. + # If running manually, there is no need to provide the scripts_folder_path. + var.scripts_folder_path != null ? + "${var.scripts_folder_path}/example_script.sh" : + "${abspath(path.module)}/scripts/example_script.sh" + ) +} + +action "local_command" "bash_test" { + config { + command = "bash" + arguments = concat([local.test_script], var.arguments) + stdin = var.stdin + + working_directory = var.working_directory + } +} diff --git a/internal/provider/testdata/TestLocalCommandAction/scripts/example_script.sh b/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh similarity index 55% rename from internal/provider/testdata/TestLocalCommandAction/scripts/example_script.sh rename to internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh index 536090ae..6d9180ba 100644 --- a/internal/provider/testdata/TestLocalCommandAction/scripts/example_script.sh +++ b/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh @@ -2,3 +2,5 @@ NAME=$(> test_file.txt From d3ee9319ffb6320b0ea9768e4fc3acea9c79832d Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 31 Oct 2025 16:45:33 -0400 Subject: [PATCH 05/16] add tests --- internal/provider/action_local_command.go | 8 +- .../provider/action_local_command_test.go | 203 ++++++++++++++++-- .../TestLocalCommandAction_bash/main.tf | 21 +- .../scripts/example_script.sh | 2 +- .../TestLocalCommandAction_bash/variables.tf | 24 +++ .../TestLocalCommandAction_stderr/main.tf | 31 +++ .../scripts/example_script.sh | 4 + 7 files changed, 253 insertions(+), 40 deletions(-) create mode 100644 internal/provider/testdata/TestLocalCommandAction_bash/variables.tf create mode 100644 internal/provider/testdata/TestLocalCommandAction_stderr/main.tf create mode 100644 internal/provider/testdata/TestLocalCommandAction_stderr/scripts/example_script.sh diff --git a/internal/provider/action_local_command.go b/internal/provider/action_local_command.go index bdd56696..39b5ef11 100644 --- a/internal/provider/action_local_command.go +++ b/internal/provider/action_local_command.go @@ -123,7 +123,7 @@ func (a *localCommandAction) Invoke(ctx context.Context, req action.InvokeReques "\n\n"+ fmt.Sprintf("Commmand: %s\n", cmd.String())+ fmt.Sprintf("Command Error: %s\n", stderrStr)+ - fmt.Sprintf("Error Message: %s", err), + fmt.Sprintf("State: %s", err), ) return } @@ -136,7 +136,7 @@ func (a *localCommandAction) Invoke(ctx context.Context, req action.InvokeReques // Send the STDOUT to Terraform to display to the practitioner. The underlying action protocol supports streaming the // STDOUT line-by-line in real-time, although each progress message gets a prefix per line, so it'd be difficult - // to read without batching lines together with an arbitrary time interval. (we can do this later if needed) + // to read without batching lines together with an arbitrary time interval (this can be improved later if needed). resp.SendProgress(action.InvokeProgressEvent{ Message: fmt.Sprintf("\n\n%s\n", stdoutStr), }) @@ -164,7 +164,7 @@ func findCommand(command string) diag.Diagnostic { "\n\n"+ fmt.Sprintf("Platform: %s\n", runtime.GOOS)+ fmt.Sprintf("Command: %s\n", command)+ - fmt.Sprintf("Error Message: %s", err), + fmt.Sprintf("Error: %s", err), ) } @@ -178,6 +178,6 @@ func genericCommandDiag(cmd *exec.Cmd, err error) diag.Diagnostic { "The action received an unexpected error while attempting to execute the command."+ "\n\n"+ fmt.Sprintf("Commmand: %s\n", cmd.Path)+ - fmt.Sprintf("Error Message: %s", err), + fmt.Sprintf("Error: %s", err), ) } diff --git a/internal/provider/action_local_command_test.go b/internal/provider/action_local_command_test.go index f52857f6..39961c55 100644 --- a/internal/provider/action_local_command_test.go +++ b/internal/provider/action_local_command_test.go @@ -7,7 +7,9 @@ import ( "fmt" "math/rand" "os" + "os/exec" "path/filepath" + "regexp" "testing" "github.com/google/go-cmp/cmp" @@ -17,22 +19,91 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -// This test calls the "bash" command and passes the STDIN and arguments to a bash script -// that prints to STDOUT and creates a file in the temporary test directory. +var ( + bashTestDirectory = filepath.Join("testdata", "TestLocalCommandAction_bash") +) + func TestLocalCommandAction_bash(t *testing.T) { wd, err := os.Getwd() if err != nil { t.Fatalf("Failed to get working directory: %v", err) } - testScriptsDir := filepath.Join(wd, "testdata", t.Name(), "scripts") + testScriptsDir := filepath.Join(wd, bashTestDirectory, "scripts") + tempDir := t.TempDir() + expectedFileContent := "stdin: , args: \n" + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "working_directory": config.StringVariable(tempDir), + "scripts_folder_path": config.StringVariable(testScriptsDir), + }, + ConfigDirectory: config.StaticDirectory(bashTestDirectory), + ActionChecks: []actioncheck.ActionCheck{ + actioncheck.ExpectProgressCount("local_command", 1), + actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), + }, + PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + }, + }, + }) +} + +func TestLocalCommandAction_bash_stdin(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + testScriptsDir := filepath.Join(wd, bashTestDirectory, "scripts") + tempDir := t.TempDir() + stdin := "John" + expectedFileContent := fmt.Sprintf("stdin: %s, args: \n", stdin) + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "stdin": config.StringVariable(stdin), + "working_directory": config.StringVariable(tempDir), + "scripts_folder_path": config.StringVariable(testScriptsDir), + }, + ConfigDirectory: config.StaticDirectory(bashTestDirectory), + ActionChecks: []actioncheck.ActionCheck{ + actioncheck.ExpectProgressCount("local_command", 1), + actioncheck.ExpectProgressMessageContains("local_command", fmt.Sprintf("Hello %s!", stdin)), + }, + PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + }, + }, + }) +} + +func TestLocalCommandAction_bash_all(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + testScriptsDir := filepath.Join(wd, bashTestDirectory, "scripts") tempDir := t.TempDir() stdin := "John" randomNumber1 := rand.Intn(100) randomNumber2 := rand.Intn(100) randomNumber3 := rand.Intn(100) - - expectedFileContent := fmt.Sprintf("%s - args: %d %d %d\n", stdin, randomNumber1, randomNumber2, randomNumber3) + expectedFileContent := fmt.Sprintf("stdin: %s, args: %d %d %d\n", stdin, randomNumber1, randomNumber2, randomNumber3) resource.UnitTest(t, resource.TestCase{ // Actions are only available in 1.14 and later @@ -52,22 +123,124 @@ func TestLocalCommandAction_bash(t *testing.T) { config.IntegerVariable(randomNumber3), ), }, - ConfigDirectory: config.TestNameDirectory(), + ConfigDirectory: config.StaticDirectory(bashTestDirectory), ActionChecks: []actioncheck.ActionCheck{ actioncheck.ExpectProgressCount("local_command", 1), actioncheck.ExpectProgressMessageContains("local_command", fmt.Sprintf("Hello %s!", stdin)), }, - PostApplyFunc: func() { - testFile, err := os.ReadFile(filepath.Join(tempDir, "test_file.txt")) - if err != nil { - t.Fatalf("error trying to read created test file: %s", err) - } - - if diff := cmp.Diff(expectedFileContent, string(testFile)); diff != "" { - t.Fatalf("unexpected file diff (-expected, +got): %s", diff) - } + PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + }, + }, + }) +} + +func TestLocalCommandAction_absolute_path_bash(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + testScriptsDir := filepath.Join(wd, bashTestDirectory, "scripts") + tempDir := t.TempDir() + expectedFileContent := "stdin: , args: \n" + + bashAbsPath, err := exec.LookPath("bash") + if err != nil { + t.Fatalf("Failed to find bash executable: %v", err) + } + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "bash_path": config.StringVariable(bashAbsPath), + "working_directory": config.StringVariable(tempDir), + "scripts_folder_path": config.StringVariable(testScriptsDir), + }, + ConfigDirectory: config.StaticDirectory(bashTestDirectory), + ActionChecks: []actioncheck.ActionCheck{ + actioncheck.ExpectProgressCount("local_command", 1), + actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), + }, + PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + }, + }, + }) +} + +func TestLocalCommandAction_not_found(t *testing.T) { + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: ` +resource "terraform_data" "test" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.local_command.test] + } + } +} + +action "local_command" "test" { + config { + command = "notarealcommand" + } +}`, + ExpectError: regexp.MustCompile(`Error: exec: "notarealcommand": executable file not found in \$PATH`), + }, + }, + }) +} + +func TestLocalCommandAction_stderr(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + testScriptsDir := filepath.Join(wd, "testdata", t.Name(), "scripts") + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "scripts_folder_path": config.StringVariable(testScriptsDir), }, + ConfigDirectory: config.TestNameDirectory(), + ExpectError: regexp.MustCompile(`Command Error: ru roh, an error occurred in the bash script!\n\nState: exit status 1`), }, }, }) } + +func assertTestFile(t *testing.T, filePath, expectedContent string) func() { + return func() { + t.Helper() + + testFile, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("error trying to read created test file: %s", err) + } + + if diff := cmp.Diff(expectedContent, string(testFile)); diff != "" { + t.Fatalf("unexpected file diff (-expected, +got): %s", diff) + } + } +} diff --git a/internal/provider/testdata/TestLocalCommandAction_bash/main.tf b/internal/provider/testdata/TestLocalCommandAction_bash/main.tf index 82a5df83..c30b8298 100644 --- a/internal/provider/testdata/TestLocalCommandAction_bash/main.tf +++ b/internal/provider/testdata/TestLocalCommandAction_bash/main.tf @@ -1,22 +1,3 @@ -variable "stdin" { - type = string -} - -variable "arguments" { - type = list(string) - default = [] -} - -variable "working_directory" { - type = string - default = null -} - -variable "scripts_folder_path" { - type = string - default = null -} - resource "terraform_data" "test" { lifecycle { action_trigger { @@ -39,7 +20,7 @@ locals { action "local_command" "bash_test" { config { - command = "bash" + command = var.bash_path arguments = concat([local.test_script], var.arguments) stdin = var.stdin diff --git a/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh b/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh index 6d9180ba..dc72210e 100644 --- a/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh +++ b/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh @@ -3,4 +3,4 @@ NAME=$(> test_file.txt +echo "stdin: $NAME, args: $@" >> test_file.txt diff --git a/internal/provider/testdata/TestLocalCommandAction_bash/variables.tf b/internal/provider/testdata/TestLocalCommandAction_bash/variables.tf new file mode 100644 index 00000000..5acbe2b8 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_bash/variables.tf @@ -0,0 +1,24 @@ +variable "bash_path" { + type = string + default = "bash" +} + +variable "stdin" { + type = string + default = null +} + +variable "arguments" { + type = list(string) + default = [] +} + +variable "working_directory" { + type = string + default = null +} + +variable "scripts_folder_path" { + type = string + default = null +} diff --git a/internal/provider/testdata/TestLocalCommandAction_stderr/main.tf b/internal/provider/testdata/TestLocalCommandAction_stderr/main.tf new file mode 100644 index 00000000..f6249ae7 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_stderr/main.tf @@ -0,0 +1,31 @@ +variable "scripts_folder_path" { + type = string + default = null +} + +resource "terraform_data" "test" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.local_command.bash_test] + } + } +} + +locals { + test_script = ( + # This configuration will get copied to a temporary location without the scripts folder, so for + # acceptance tests we pass the folder path from the Go test environment via a variable. + # If running manually, there is no need to provide the scripts_folder_path. + var.scripts_folder_path != null ? + "${var.scripts_folder_path}/example_script.sh" : + "${abspath(path.module)}/scripts/example_script.sh" + ) +} + +action "local_command" "bash_test" { + config { + command = "bash" + arguments = [local.test_script] + } +} diff --git a/internal/provider/testdata/TestLocalCommandAction_stderr/scripts/example_script.sh b/internal/provider/testdata/TestLocalCommandAction_stderr/scripts/example_script.sh new file mode 100644 index 00000000..6d770d5f --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_stderr/scripts/example_script.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo "ru roh, an error occurred in the bash script!" >&2 +exit 1 \ No newline at end of file From 7021b8c93234046188a2cb43774b00148f6bacbf Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 3 Nov 2025 09:44:17 -0500 Subject: [PATCH 06/16] don't encode null arguments --- internal/provider/action_local_command.go | 11 +++-- .../provider/action_local_command_test.go | 41 +++++++++++++++++++ .../main.tf | 29 +++++++++++++ .../scripts/example_script.sh | 11 +++++ .../variables.tf | 24 +++++++++++ 5 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 internal/provider/testdata/TestLocalCommandAction_bash_null_args/main.tf create mode 100644 internal/provider/testdata/TestLocalCommandAction_bash_null_args/scripts/example_script.sh create mode 100644 internal/provider/testdata/TestLocalCommandAction_bash_null_args/variables.tf diff --git a/internal/provider/action_local_command.go b/internal/provider/action_local_command.go index 39b5ef11..c352ec1f 100644 --- a/internal/provider/action_local_command.go +++ b/internal/provider/action_local_command.go @@ -91,9 +91,14 @@ func (a *localCommandAction) Invoke(ctx context.Context, req action.InvokeReques } arguments := make([]string, 0) - resp.Diagnostics.Append(config.Arguments.ElementsAs(ctx, &arguments, true)...) - if resp.Diagnostics.HasError() { - return + for _, element := range config.Arguments.Elements() { + strElement, ok := element.(types.String) + // Mirroring the underlying os/exec Command support for args (no nil arguments, but does support empty strings) + if element.IsNull() || !ok { + continue + } + + arguments = append(arguments, strElement.ValueString()) } cmd := exec.CommandContext(ctx, command, arguments...) diff --git a/internal/provider/action_local_command_test.go b/internal/provider/action_local_command_test.go index 39961c55..c16e9c0d 100644 --- a/internal/provider/action_local_command_test.go +++ b/internal/provider/action_local_command_test.go @@ -134,6 +134,47 @@ func TestLocalCommandAction_bash_all(t *testing.T) { }) } +func TestLocalCommandAction_bash_null_args(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + testScriptsDir := filepath.Join(wd, "testdata", t.Name(), "scripts") + tempDir := t.TempDir() + randomNumber1 := rand.Intn(100) + randomNumber2 := rand.Intn(100) + randomNumber3 := rand.Intn(100) + expectedFileContent := fmt.Sprintf("stdin: , args: %d %d %d\n", randomNumber1, randomNumber2, randomNumber3) + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "working_directory": config.StringVariable(tempDir), + "scripts_folder_path": config.StringVariable(testScriptsDir), + "arguments": config.ListVariable( // Null arguments will be appended to this list in the test config, then filtered by the action code. + config.IntegerVariable(randomNumber1), + config.IntegerVariable(randomNumber2), + config.IntegerVariable(randomNumber3), + ), + }, + ConfigDirectory: config.TestNameDirectory(), + ActionChecks: []actioncheck.ActionCheck{ + actioncheck.ExpectProgressCount("local_command", 1), + actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), + }, + PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + }, + }, + }) +} + func TestLocalCommandAction_absolute_path_bash(t *testing.T) { wd, err := os.Getwd() if err != nil { diff --git a/internal/provider/testdata/TestLocalCommandAction_bash_null_args/main.tf b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/main.tf new file mode 100644 index 00000000..d6e92bb3 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/main.tf @@ -0,0 +1,29 @@ +resource "terraform_data" "test" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.local_command.bash_test] + } + } +} + +locals { + test_script = ( + # This configuration will get copied to a temporary location without the scripts folder, so for + # acceptance tests we pass the folder path from the Go test environment via a variable. + # If running manually, there is no need to provide the scripts_folder_path. + var.scripts_folder_path != null ? + "${var.scripts_folder_path}/example_script.sh" : + "${abspath(path.module)}/scripts/example_script.sh" + ) +} + +action "local_command" "bash_test" { + config { + command = var.bash_path + arguments = concat([local.test_script], [null, null], var.arguments, [null, null, null]) # null arguments will be removed, empty strings preserved + stdin = var.stdin + + working_directory = var.working_directory + } +} diff --git a/internal/provider/testdata/TestLocalCommandAction_bash_null_args/scripts/example_script.sh b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/scripts/example_script.sh new file mode 100644 index 00000000..a6bce503 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/scripts/example_script.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +if [ "$#" -ne 3 ]; then + echo "You provided $# arguments, expected exactly 3 random number arguments (the 5 null arguments should be removed)." >&2 + exit 1 +fi + +NAME=$(> test_file.txt \ No newline at end of file diff --git a/internal/provider/testdata/TestLocalCommandAction_bash_null_args/variables.tf b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/variables.tf new file mode 100644 index 00000000..5acbe2b8 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/variables.tf @@ -0,0 +1,24 @@ +variable "bash_path" { + type = string + default = "bash" +} + +variable "stdin" { + type = string + default = null +} + +variable "arguments" { + type = list(string) + default = [] +} + +variable "working_directory" { + type = string + default = null +} + +variable "scripts_folder_path" { + type = string + default = null +} From 34d5922fc80c92e94bb607ca14242ab0f7681c7c Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 3 Nov 2025 10:45:35 -0500 Subject: [PATCH 07/16] add copyright headers --- internal/provider/testdata/TestLocalCommandAction_bash/main.tf | 3 +++ .../TestLocalCommandAction_bash/scripts/example_script.sh | 3 +++ .../provider/testdata/TestLocalCommandAction_bash/variables.tf | 3 +++ .../testdata/TestLocalCommandAction_bash_null_args/main.tf | 3 +++ .../scripts/example_script.sh | 3 +++ .../TestLocalCommandAction_bash_null_args/variables.tf | 3 +++ .../provider/testdata/TestLocalCommandAction_stderr/main.tf | 3 +++ .../TestLocalCommandAction_stderr/scripts/example_script.sh | 3 +++ 8 files changed, 24 insertions(+) diff --git a/internal/provider/testdata/TestLocalCommandAction_bash/main.tf b/internal/provider/testdata/TestLocalCommandAction_bash/main.tf index c30b8298..25915cc0 100644 --- a/internal/provider/testdata/TestLocalCommandAction_bash/main.tf +++ b/internal/provider/testdata/TestLocalCommandAction_bash/main.tf @@ -1,3 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + resource "terraform_data" "test" { lifecycle { action_trigger { diff --git a/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh b/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh index dc72210e..d512909c 100644 --- a/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh +++ b/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh @@ -1,4 +1,7 @@ #!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + NAME=$(&2 diff --git a/internal/provider/testdata/TestLocalCommandAction_bash_null_args/variables.tf b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/variables.tf index 5acbe2b8..c5b16261 100644 --- a/internal/provider/testdata/TestLocalCommandAction_bash_null_args/variables.tf +++ b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/variables.tf @@ -1,3 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + variable "bash_path" { type = string default = "bash" diff --git a/internal/provider/testdata/TestLocalCommandAction_stderr/main.tf b/internal/provider/testdata/TestLocalCommandAction_stderr/main.tf index f6249ae7..a6284fce 100644 --- a/internal/provider/testdata/TestLocalCommandAction_stderr/main.tf +++ b/internal/provider/testdata/TestLocalCommandAction_stderr/main.tf @@ -1,3 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + variable "scripts_folder_path" { type = string default = null diff --git a/internal/provider/testdata/TestLocalCommandAction_stderr/scripts/example_script.sh b/internal/provider/testdata/TestLocalCommandAction_stderr/scripts/example_script.sh index 6d770d5f..c0d72bad 100644 --- a/internal/provider/testdata/TestLocalCommandAction_stderr/scripts/example_script.sh +++ b/internal/provider/testdata/TestLocalCommandAction_stderr/scripts/example_script.sh @@ -1,4 +1,7 @@ #!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + echo "ru roh, an error occurred in the bash script!" >&2 exit 1 \ No newline at end of file From d8fd7cea8a52be4690ba691fef0e62834d6d91f6 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 3 Nov 2025 14:53:11 -0500 Subject: [PATCH 08/16] add documentation and example --- docs/actions/command.md | 79 +++++++++++++++++++ examples/actions/local_command/action.tf | 19 +++++ .../actions/local_command/example_script.sh | 4 + internal/provider/action_local_command.go | 14 ++-- templates/actions/command.md.tmpl | 45 +++++++++++ 5 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 docs/actions/command.md create mode 100644 examples/actions/local_command/action.tf create mode 100644 examples/actions/local_command/example_script.sh create mode 100644 templates/actions/command.md.tmpl diff --git a/docs/actions/command.md b/docs/actions/command.md new file mode 100644 index 00000000..d387ac49 --- /dev/null +++ b/docs/actions/command.md @@ -0,0 +1,79 @@ +page_title: "local_command Action - terraform-provider-local" +subcategory: "" +description: |- + Invokes an executable on the local machine. All environment variables visible to the Terraform process are passed through to the child process. After the child process successfully executes, the stdout will be returned for Terraform to display to the user. + Any non-zero exit code will be treated as an error and will return a diagnostic to Terraform containing the stderr message if available. +--- + +# local_command (Action) + +Invokes an executable on the local machine. All environment variables visible to the Terraform process are passed through to the child process. After the child process successfully executes, the `stdout` will be returned for Terraform to display to the user. + +Any non-zero exit code will be treated as an error and will return a diagnostic to Terraform containing the `stderr` message if available. + +## Example Usage + +For the following bash script (`example_script.sh`): +```bash +#!/bin/bash + +DATA=$( +## Schema + +### Required + +- `command` (String) Executable name to be discovered on the PATH or absolute path to executable. + +### Optional + +- `arguments` (List of String) Arguments to be passed to the given command. Any `null` arguments will be removed from the list. +- `stdin` (String) Data to be passed to the given command's standard input. +- `working_directory` (String) The directory where the command should be executed. Defaults to the Terraform working directory. diff --git a/examples/actions/local_command/action.tf b/examples/actions/local_command/action.tf new file mode 100644 index 00000000..0accb220 --- /dev/null +++ b/examples/actions/local_command/action.tf @@ -0,0 +1,19 @@ +resource "terraform_data" "test" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.local_command.bash_example] + } + } +} + +action "local_command" "bash_example" { + config { + command = "bash" + arguments = ["example_script.sh", "arg1", "arg2"] + stdin = jsonencode({ + "key1" : "value1" + "key2" : "value2" + }) + } +} diff --git a/examples/actions/local_command/example_script.sh b/examples/actions/local_command/example_script.sh new file mode 100644 index 00000000..afe4b63b --- /dev/null +++ b/examples/actions/local_command/example_script.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +DATA=$( Date: Mon, 3 Nov 2025 15:19:00 -0500 Subject: [PATCH 09/16] remove the tests temporarily for the CI --- go.mod | 4 +- go.sum | 2 + .../provider/action_local_command_test.go | 51 +++++++++++-------- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 37c2570f..c914ed8e 100644 --- a/go.mod +++ b/go.mod @@ -2,15 +2,13 @@ module github.com/terraform-providers/terraform-provider-local go 1.24.0 -replace github.com/hashicorp/terraform-plugin-testing => /Users/austin.valle/code/terraform-plugin-testing - require ( github.com/google/go-cmp v0.7.0 github.com/hashicorp/terraform-plugin-framework v1.16.1 github.com/hashicorp/terraform-plugin-framework-validators v0.19.0 github.com/hashicorp/terraform-plugin-go v0.29.0 github.com/hashicorp/terraform-plugin-log v0.9.0 - github.com/hashicorp/terraform-plugin-testing v1.13.3 + github.com/hashicorp/terraform-plugin-testing v1.14.0-beta.1.0.20251029152858-203e6cc410a0 ) require ( diff --git a/go.sum b/go.sum index d1dbb316..e52877eb 100644 --- a/go.sum +++ b/go.sum @@ -93,6 +93,8 @@ github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9T github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4= github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1/go.mod h1:GQhpKVvvuwzD79e8/NZ+xzj+ZpWovdPAe8nfV/skwNU= +github.com/hashicorp/terraform-plugin-testing v1.14.0-beta.1.0.20251029152858-203e6cc410a0 h1:2ZfVb9DwefNk/aN3uJZLIADETfQOdxtOrDZ6iLGtx8o= +github.com/hashicorp/terraform-plugin-testing v1.14.0-beta.1.0.20251029152858-203e6cc410a0/go.mod h1:UrIjRAJLN0kygs0miY1Moy4PxUzy2e9R5WxyRk8aliI= github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= diff --git a/internal/provider/action_local_command_test.go b/internal/provider/action_local_command_test.go index c16e9c0d..eae8a24a 100644 --- a/internal/provider/action_local_command_test.go +++ b/internal/provider/action_local_command_test.go @@ -13,7 +13,6 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-testing/actioncheck" "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/tfversion" @@ -46,10 +45,12 @@ func TestLocalCommandAction_bash(t *testing.T) { "scripts_folder_path": config.StringVariable(testScriptsDir), }, ConfigDirectory: config.StaticDirectory(bashTestDirectory), - ActionChecks: []actioncheck.ActionCheck{ - actioncheck.ExpectProgressCount("local_command", 1), - actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), - }, + // TODO: Currently action checks don't exist, but eventually we can run these on the progress messages + // https://github.com/hashicorp/terraform-plugin-testing/pull/570 + // ActionChecks: []actioncheck.ActionCheck{ + // actioncheck.ExpectProgressCount("local_command", 1), + // actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), + // }, PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), }, }, @@ -81,10 +82,12 @@ func TestLocalCommandAction_bash_stdin(t *testing.T) { "scripts_folder_path": config.StringVariable(testScriptsDir), }, ConfigDirectory: config.StaticDirectory(bashTestDirectory), - ActionChecks: []actioncheck.ActionCheck{ - actioncheck.ExpectProgressCount("local_command", 1), - actioncheck.ExpectProgressMessageContains("local_command", fmt.Sprintf("Hello %s!", stdin)), - }, + // TODO: Currently action checks don't exist, but eventually we can run these on the progress messages + // https://github.com/hashicorp/terraform-plugin-testing/pull/570 + // ActionChecks: []actioncheck.ActionCheck{ + // actioncheck.ExpectProgressCount("local_command", 1), + // actioncheck.ExpectProgressMessageContains("local_command", fmt.Sprintf("Hello %s!", stdin)), + // }, PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), }, }, @@ -124,10 +127,12 @@ func TestLocalCommandAction_bash_all(t *testing.T) { ), }, ConfigDirectory: config.StaticDirectory(bashTestDirectory), - ActionChecks: []actioncheck.ActionCheck{ - actioncheck.ExpectProgressCount("local_command", 1), - actioncheck.ExpectProgressMessageContains("local_command", fmt.Sprintf("Hello %s!", stdin)), - }, + // TODO: Currently action checks don't exist, but eventually we can run these on the progress messages + // https://github.com/hashicorp/terraform-plugin-testing/pull/570 + // ActionChecks: []actioncheck.ActionCheck{ + // actioncheck.ExpectProgressCount("local_command", 1), + // actioncheck.ExpectProgressMessageContains("local_command", fmt.Sprintf("Hello %s!", stdin)), + // }, PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), }, }, @@ -165,10 +170,12 @@ func TestLocalCommandAction_bash_null_args(t *testing.T) { ), }, ConfigDirectory: config.TestNameDirectory(), - ActionChecks: []actioncheck.ActionCheck{ - actioncheck.ExpectProgressCount("local_command", 1), - actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), - }, + // TODO: Currently action checks don't exist, but eventually we can run these on the progress messages + // https://github.com/hashicorp/terraform-plugin-testing/pull/570 + // ActionChecks: []actioncheck.ActionCheck{ + // actioncheck.ExpectProgressCount("local_command", 1), + // actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), + // }, PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), }, }, @@ -204,10 +211,12 @@ func TestLocalCommandAction_absolute_path_bash(t *testing.T) { "scripts_folder_path": config.StringVariable(testScriptsDir), }, ConfigDirectory: config.StaticDirectory(bashTestDirectory), - ActionChecks: []actioncheck.ActionCheck{ - actioncheck.ExpectProgressCount("local_command", 1), - actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), - }, + // TODO: Currently action checks don't exist, but eventually we can run these on the progress messages + // https://github.com/hashicorp/terraform-plugin-testing/pull/570 + // ActionChecks: []actioncheck.ActionCheck{ + // actioncheck.ExpectProgressCount("local_command", 1), + // actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), + // }, PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), }, }, From 516a8fe68748d54771b6a7c579a8b1d26c54fa8f Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 3 Nov 2025 15:31:25 -0500 Subject: [PATCH 10/16] lint --- internal/provider/action_local_command.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/provider/action_local_command.go b/internal/provider/action_local_command.go index 98116f36..f2ed3a43 100644 --- a/internal/provider/action_local_command.go +++ b/internal/provider/action_local_command.go @@ -85,7 +85,7 @@ func (a *localCommandAction) Invoke(ctx context.Context, req action.InvokeReques return } - // Prep the commmand + // Prep the command command := config.Command.ValueString() resp.Diagnostics.Append(findCommand(command)) if resp.Diagnostics.HasError() { @@ -128,7 +128,7 @@ func (a *localCommandAction) Invoke(ctx context.Context, req action.InvokeReques "Command Execution Failed", "The action received an unexpected error while attempting to execute the command."+ "\n\n"+ - fmt.Sprintf("Commmand: %s\n", cmd.String())+ + fmt.Sprintf("Command: %s\n", cmd.String())+ fmt.Sprintf("Command Error: %s\n", stderrStr)+ fmt.Sprintf("State: %s", err), ) @@ -184,7 +184,7 @@ func genericCommandDiag(cmd *exec.Cmd, err error) diag.Diagnostic { "Command Execution Failed", "The action received an unexpected error while attempting to execute the command."+ "\n\n"+ - fmt.Sprintf("Commmand: %s\n", cmd.Path)+ + fmt.Sprintf("Command: %s\n", cmd.Path)+ fmt.Sprintf("Error: %s", err), ) } From 701af8702ecfa7274af116e9df0ca785269a6a58 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 3 Nov 2025 15:37:17 -0500 Subject: [PATCH 11/16] use rc1 for generating docs --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1094f110..55f78752 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,6 +37,8 @@ jobs: - name: Set up Terraform uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 with: + # TODO: Remove once Terraform 1.14 GA is released + terraform_version: 1.14.0-rc1 terraform_wrapper: false - name: Generate From 7bb167cf41366dff61f1a0a96b146195c3098914 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 3 Nov 2025 15:48:53 -0500 Subject: [PATCH 12/16] add changelog and fix tests for windows --- .changes/unreleased/FEATURES-20251103-153952.yaml | 5 +++++ internal/provider/action_local_command_test.go | 2 +- .../TestLocalCommandAction_bash/scripts/example_script.sh | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/FEATURES-20251103-153952.yaml diff --git a/.changes/unreleased/FEATURES-20251103-153952.yaml b/.changes/unreleased/FEATURES-20251103-153952.yaml new file mode 100644 index 00000000..6b6cc9f9 --- /dev/null +++ b/.changes/unreleased/FEATURES-20251103-153952.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'action/local_command: New action that invokes an executable on the local machine.' +time: 2025-11-03T15:39:52.741799-05:00 +custom: + Issue: "450" diff --git a/internal/provider/action_local_command_test.go b/internal/provider/action_local_command_test.go index eae8a24a..388432d7 100644 --- a/internal/provider/action_local_command_test.go +++ b/internal/provider/action_local_command_test.go @@ -248,7 +248,7 @@ action "local_command" "test" { command = "notarealcommand" } }`, - ExpectError: regexp.MustCompile(`Error: exec: "notarealcommand": executable file not found in \$PATH`), + ExpectError: regexp.MustCompile(`Error: exec: "notarealcommand": executable file not found`), }, }, }) diff --git a/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh b/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh index d512909c..064e4084 100644 --- a/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh +++ b/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MPL-2.0 -NAME=$(> test_file.txt From 9c79e8c2b3279476acc7babafb459f1e13527ac8 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 3 Nov 2025 16:25:10 -0500 Subject: [PATCH 13/16] add vscode gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 5982d2c6..98c77a08 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ website/vendor # Test exclusions !command/test-fixtures/**/*.tfstate !command/test-fixtures/**/.terraform/ + +.vscode \ No newline at end of file From 2a237a10cb9e4e4f0f8c636a093bf79355a4bee5 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 4 Nov 2025 15:23:01 -0500 Subject: [PATCH 14/16] refactor --- internal/provider/action_local_command.go | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/internal/provider/action_local_command.go b/internal/provider/action_local_command.go index f2ed3a43..bf374897 100644 --- a/internal/provider/action_local_command.go +++ b/internal/provider/action_local_command.go @@ -135,7 +135,14 @@ func (a *localCommandAction) Invoke(ctx context.Context, req action.InvokeReques return } - resp.Diagnostics.Append(genericCommandDiag(cmd, err)) + resp.Diagnostics.AddAttributeError( + path.Root("command"), + "Command Execution Failed", + "The action received an unexpected error while attempting to execute the command."+ + "\n\n"+ + fmt.Sprintf("Command: %s\n", cmd.Path)+ + fmt.Sprintf("Error: %s", err), + ) return } @@ -177,14 +184,3 @@ func findCommand(command string) diag.Diagnostic { return nil } - -func genericCommandDiag(cmd *exec.Cmd, err error) diag.Diagnostic { - return diag.NewAttributeErrorDiagnostic( - path.Root("command"), - "Command Execution Failed", - "The action received an unexpected error while attempting to execute the command."+ - "\n\n"+ - fmt.Sprintf("Command: %s\n", cmd.Path)+ - fmt.Sprintf("Error: %s", err), - ) -} From f811be978fec421f36b17fe7ea6f9ae113a7deaf Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 10 Nov 2025 10:34:29 -0500 Subject: [PATCH 15/16] update docs --- docs/actions/command.md | 2 +- internal/provider/action_local_command.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/actions/command.md b/docs/actions/command.md index d387ac49..3be7d075 100644 --- a/docs/actions/command.md +++ b/docs/actions/command.md @@ -76,4 +76,4 @@ Apply complete! Resources: 1 added, 0 changed, 0 destroyed. - `arguments` (List of String) Arguments to be passed to the given command. Any `null` arguments will be removed from the list. - `stdin` (String) Data to be passed to the given command's standard input. -- `working_directory` (String) The directory where the command should be executed. Defaults to the Terraform working directory. +- `working_directory` (String) The directory path where the command should be executed, either an absolute path or relative to the Terraform working directory. If not provided, defaults to the Terraform working directory. diff --git a/internal/provider/action_local_command.go b/internal/provider/action_local_command.go index bf374897..2dc3d49a 100644 --- a/internal/provider/action_local_command.go +++ b/internal/provider/action_local_command.go @@ -54,7 +54,7 @@ func (a *localCommandAction) Schema(ctx context.Context, req action.SchemaReques Optional: true, }, "working_directory": schema.StringAttribute{ - Description: "The directory where the command should be executed. Defaults to the Terraform working directory.", + Description: "The directory path where the command should be executed, either an absolute path or relative to the Terraform working directory. If not provided, defaults to the Terraform working directory.", Optional: true, }, }, From 829e7548368113032d2cda11415d75f6466173dc Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 13 Nov 2025 09:47:49 -0500 Subject: [PATCH 16/16] replace unreleased plugin testing code w/ TODOs and normal `Check` funcs --- .github/workflows/test.yml | 2 +- go.mod | 4 +- go.sum | 4 +- internal/provider/action_local_command.go | 1 - .../provider/action_local_command_test.go | 114 ++++++++++-------- 5 files changed, 67 insertions(+), 58 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55f78752..1a8b686c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 with: # TODO: Remove once Terraform 1.14 GA is released - terraform_version: 1.14.0-rc1 + terraform_version: 1.14.0-rc2 terraform_wrapper: false - name: Generate diff --git a/go.mod b/go.mod index c914ed8e..ef68e75c 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.24.0 require ( github.com/google/go-cmp v0.7.0 + github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-plugin-framework v1.16.1 github.com/hashicorp/terraform-plugin-framework-validators v0.19.0 github.com/hashicorp/terraform-plugin-go v0.29.0 github.com/hashicorp/terraform-plugin-log v0.9.0 - github.com/hashicorp/terraform-plugin-testing v1.14.0-beta.1.0.20251029152858-203e6cc410a0 + github.com/hashicorp/terraform-plugin-testing v1.13.3 ) require ( @@ -27,7 +28,6 @@ require ( github.com/hashicorp/go-plugin v1.7.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hc-install v0.9.2 // indirect github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect diff --git a/go.sum b/go.sum index e52877eb..13f88458 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9T github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4= github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1/go.mod h1:GQhpKVvvuwzD79e8/NZ+xzj+ZpWovdPAe8nfV/skwNU= -github.com/hashicorp/terraform-plugin-testing v1.14.0-beta.1.0.20251029152858-203e6cc410a0 h1:2ZfVb9DwefNk/aN3uJZLIADETfQOdxtOrDZ6iLGtx8o= -github.com/hashicorp/terraform-plugin-testing v1.14.0-beta.1.0.20251029152858-203e6cc410a0/go.mod h1:UrIjRAJLN0kygs0miY1Moy4PxUzy2e9R5WxyRk8aliI= +github.com/hashicorp/terraform-plugin-testing v1.13.3 h1:QLi/khB8Z0a5L54AfPrHukFpnwsGL8cwwswj4RZduCo= +github.com/hashicorp/terraform-plugin-testing v1.13.3/go.mod h1:WHQ9FDdiLoneey2/QHpGM/6SAYf4A7AZazVg7230pLE= github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= diff --git a/internal/provider/action_local_command.go b/internal/provider/action_local_command.go index 2dc3d49a..0b4135cd 100644 --- a/internal/provider/action_local_command.go +++ b/internal/provider/action_local_command.go @@ -35,7 +35,6 @@ func (a *localCommandAction) Metadata(ctx context.Context, req action.MetadataRe func (a *localCommandAction) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) { resp.Schema = schema.Schema{ - // TODO: Once we have a local_command data source, reference that to be used if the user needs to the consume the output of the command (and it's idempotent) MarkdownDescription: "Invokes an executable on the local machine. All environment variables visible to the Terraform process are passed through " + "to the child process. After the child process successfully executes, the `stdout` will be returned for Terraform to display to the user.\n\n" + "Any non-zero exit code will be treated as an error and will return a diagnostic to Terraform containing the `stderr` message if available.", diff --git a/internal/provider/action_local_command_test.go b/internal/provider/action_local_command_test.go index 388432d7..88dd0858 100644 --- a/internal/provider/action_local_command_test.go +++ b/internal/provider/action_local_command_test.go @@ -13,8 +13,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) @@ -35,7 +37,7 @@ func TestLocalCommandAction_bash(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Actions are only available in 1.14 and later TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released }, ProtoV5ProviderFactories: protoV5ProviderFactories(), Steps: []resource.TestStep{ @@ -45,13 +47,12 @@ func TestLocalCommandAction_bash(t *testing.T) { "scripts_folder_path": config.StringVariable(testScriptsDir), }, ConfigDirectory: config.StaticDirectory(bashTestDirectory), - // TODO: Currently action checks don't exist, but eventually we can run these on the progress messages - // https://github.com/hashicorp/terraform-plugin-testing/pull/570 - // ActionChecks: []actioncheck.ActionCheck{ - // actioncheck.ExpectProgressCount("local_command", 1), - // actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), - // }, - PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + Check: func(s *terraform.State) error { + return assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent) + }, + // TODO: use this when PostApplyFunc is released in terraform-plugin-testing + // + // PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), }, }, }) @@ -71,7 +72,7 @@ func TestLocalCommandAction_bash_stdin(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Actions are only available in 1.14 and later TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released }, ProtoV5ProviderFactories: protoV5ProviderFactories(), Steps: []resource.TestStep{ @@ -82,13 +83,11 @@ func TestLocalCommandAction_bash_stdin(t *testing.T) { "scripts_folder_path": config.StringVariable(testScriptsDir), }, ConfigDirectory: config.StaticDirectory(bashTestDirectory), - // TODO: Currently action checks don't exist, but eventually we can run these on the progress messages - // https://github.com/hashicorp/terraform-plugin-testing/pull/570 - // ActionChecks: []actioncheck.ActionCheck{ - // actioncheck.ExpectProgressCount("local_command", 1), - // actioncheck.ExpectProgressMessageContains("local_command", fmt.Sprintf("Hello %s!", stdin)), - // }, - PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + Check: func(s *terraform.State) error { + return assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent) + }, + // TODO: use this when PostApplyFunc is released in terraform-plugin-testing + // PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), }, }, }) @@ -111,7 +110,7 @@ func TestLocalCommandAction_bash_all(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Actions are only available in 1.14 and later TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released }, ProtoV5ProviderFactories: protoV5ProviderFactories(), Steps: []resource.TestStep{ @@ -127,13 +126,11 @@ func TestLocalCommandAction_bash_all(t *testing.T) { ), }, ConfigDirectory: config.StaticDirectory(bashTestDirectory), - // TODO: Currently action checks don't exist, but eventually we can run these on the progress messages - // https://github.com/hashicorp/terraform-plugin-testing/pull/570 - // ActionChecks: []actioncheck.ActionCheck{ - // actioncheck.ExpectProgressCount("local_command", 1), - // actioncheck.ExpectProgressMessageContains("local_command", fmt.Sprintf("Hello %s!", stdin)), - // }, - PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + Check: func(s *terraform.State) error { + return assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent) + }, + // TODO: use this when PostApplyFunc is released in terraform-plugin-testing + // PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), }, }, }) @@ -155,7 +152,7 @@ func TestLocalCommandAction_bash_null_args(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Actions are only available in 1.14 and later TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released }, ProtoV5ProviderFactories: protoV5ProviderFactories(), Steps: []resource.TestStep{ @@ -170,13 +167,11 @@ func TestLocalCommandAction_bash_null_args(t *testing.T) { ), }, ConfigDirectory: config.TestNameDirectory(), - // TODO: Currently action checks don't exist, but eventually we can run these on the progress messages - // https://github.com/hashicorp/terraform-plugin-testing/pull/570 - // ActionChecks: []actioncheck.ActionCheck{ - // actioncheck.ExpectProgressCount("local_command", 1), - // actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), - // }, - PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + Check: func(s *terraform.State) error { + return assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent) + }, + // TODO: use this when PostApplyFunc is released in terraform-plugin-testing + // PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), }, }, }) @@ -200,7 +195,7 @@ func TestLocalCommandAction_absolute_path_bash(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Actions are only available in 1.14 and later TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released }, ProtoV5ProviderFactories: protoV5ProviderFactories(), Steps: []resource.TestStep{ @@ -211,13 +206,11 @@ func TestLocalCommandAction_absolute_path_bash(t *testing.T) { "scripts_folder_path": config.StringVariable(testScriptsDir), }, ConfigDirectory: config.StaticDirectory(bashTestDirectory), - // TODO: Currently action checks don't exist, but eventually we can run these on the progress messages - // https://github.com/hashicorp/terraform-plugin-testing/pull/570 - // ActionChecks: []actioncheck.ActionCheck{ - // actioncheck.ExpectProgressCount("local_command", 1), - // actioncheck.ExpectProgressMessageContains("local_command", "Hello !"), - // }, - PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + Check: func(s *terraform.State) error { + return assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent) + }, + // TODO: use this when PostApplyFunc is released in terraform-plugin-testing + // PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), }, }, }) @@ -228,7 +221,7 @@ func TestLocalCommandAction_not_found(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Actions are only available in 1.14 and later TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released }, ProtoV5ProviderFactories: protoV5ProviderFactories(), Steps: []resource.TestStep{ @@ -265,7 +258,7 @@ func TestLocalCommandAction_stderr(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Actions are only available in 1.14 and later TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_14_0), + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released }, ProtoV5ProviderFactories: protoV5ProviderFactories(), Steps: []resource.TestStep{ @@ -280,17 +273,34 @@ func TestLocalCommandAction_stderr(t *testing.T) { }) } -func assertTestFile(t *testing.T, filePath, expectedContent string) func() { - return func() { - t.Helper() +// TODO: use this function when PostApplyFunc is released in terraform-plugin-testing +// +// func assertTestFile(t *testing.T, filePath, expectedContent string) func() { +// return func() { +// t.Helper() + +// testFile, err := os.ReadFile(filePath) +// if err != nil { +// t.Fatalf("error trying to read created test file: %s", err) +// } - testFile, err := os.ReadFile(filePath) - if err != nil { - t.Fatalf("error trying to read created test file: %s", err) - } +// if diff := cmp.Diff(expectedContent, string(testFile)); diff != "" { +// t.Fatalf("unexpected file diff (-expected, +got): %s", diff) +// } +// } +// } - if diff := cmp.Diff(expectedContent, string(testFile)); diff != "" { - t.Fatalf("unexpected file diff (-expected, +got): %s", diff) - } +func assertTestFile(t *testing.T, filePath, expectedContent string) error { + t.Helper() + + testFile, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("error trying to read created test file: %s", err) } + + if diff := cmp.Diff(expectedContent, string(testFile)); diff != "" { + return fmt.Errorf("unexpected file diff (-expected, +got): %s", diff) + } + + return nil }