allow creating entities when they dont exist

This commit is contained in:
Graham McIntire 2026-01-29 12:34:58 -06:00
parent 3eef94f685
commit 233be61d3e
No known key found for this signature in database
9 changed files with 2187 additions and 53 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
CLAUDE.md
terraform-provider-towerops
dist/
coverage.out

64
go.mod
View file

@ -1,30 +1,62 @@
module github.com/towerops/terraform-provider-towerops
go 1.21
require github.com/hashicorp/terraform-plugin-framework v1.4.2
go 1.24.0
require (
github.com/hashicorp/terraform-plugin-framework v1.17.0
github.com/hashicorp/terraform-plugin-go v0.29.0
github.com/hashicorp/terraform-plugin-testing v1.14.0
)
require (
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/agext/levenshtein v1.2.2 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/go-hclog v1.6.2 // indirect
github.com/hashicorp/go-plugin v1.6.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-cty v1.5.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
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/terraform-plugin-go v0.19.1 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.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
github.com/hashicorp/terraform-exec v0.24.0 // indirect
github.com/hashicorp/terraform-json v0.27.2 // indirect
github.com/hashicorp/terraform-plugin-log v0.10.0 // 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.1 // indirect
github.com/hashicorp/yamux v0.1.2 // 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.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
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
google.golang.org/grpc v1.60.1 // indirect
google.golang.org/protobuf v1.32.0 // indirect
github.com/zclconf/go-cty v1.17.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.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
google.golang.org/protobuf v1.36.9 // indirect
)

234
go.sum
View file

@ -1,37 +1,114 @@
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE=
github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I=
github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A=
github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU=
github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-cty v1.5.0 h1:EkQ/v+dDNUqnuVpmS5fPqyY71NXVgT5gf32+57xY8g0=
github.com/hashicorp/go-cty v1.5.0/go.mod h1:lFUCG5kd8exDobgSfyj4ONE/dc822kiYMguVKdHGMLM=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/terraform-plugin-framework v1.4.2 h1:P7a7VP1GZbjc4rv921Xy5OckzhoiO3ig6SGxwelD2sI=
github.com/hashicorp/terraform-plugin-framework v1.4.2/go.mod h1:GWl3InPFZi2wVQmdVnINPKys09s9mLmTZr95/ngLnbY=
github.com/hashicorp/terraform-plugin-go v0.19.1 h1:lf/jTGTeELcz5IIbn/94mJdmnTjRYm6S6ct/JqCSr50=
github.com/hashicorp/terraform-plugin-go v0.19.1/go.mod h1:5NMIS+DXkfacX6o5HCpswda5yjkSYfKzn1Nfl9l+qRs=
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-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
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.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.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.17.0 h1:JdX50CFrYcYFY31gkmitAEAzLKoBgsK+iaJjDC8OexY=
github.com/hashicorp/terraform-plugin-framework v1.17.0/go.mod h1:4OUXKdHNosX+ys6rLgVlgklfxN3WHR5VHSOABeS/BM0=
github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU=
github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM=
github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g=
github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0=
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 h1:5t4VKrjOJ0rg0sVuSJ86dz5K7PHsMO6OKrHFzDBerWA=
github.com/hashicorp/terraform-plugin-testing v1.14.0/go.mod h1:1qfWkecyYe1Do2EEOK/5/WnTyvC8wQucUkkhiGLg5nk=
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=
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
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.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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -41,41 +118,124 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
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.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=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
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.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=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
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.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=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.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=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
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=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -3,12 +3,16 @@ package provider
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
)
// ErrNotFound is returned when a resource is not found (404).
var ErrNotFound = errors.New("resource not found")
const defaultBaseURL = "https://towerops.net"
// Client is the TowerOps API client.
@ -93,6 +97,9 @@ func (c *Client) doRequest(method, path string, body interface{}) ([]byte, error
}
if resp.StatusCode >= 400 {
if resp.StatusCode == http.StatusNotFound {
return nil, ErrNotFound
}
var apiErr APIError
if err := json.Unmarshal(respBody, &apiErr); err == nil {
if apiErr.Error != "" {

View file

@ -0,0 +1,573 @@
package provider
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
)
func TestClient_ErrNotFound(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "not found"}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
_, err := client.GetDevice("nonexistent-id")
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound, got: %v", err)
}
}
func TestClient_GetDevice_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/devices/device-123" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
if r.Header.Get("Authorization") != "Bearer test-token" {
t.Errorf("unexpected auth header: %s", r.Header.Get("Authorization"))
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"id": "device-123",
"site_id": "site-456",
"ip_address": "192.168.1.1",
"name": "Test Device",
"inserted_at": "2024-01-01T00:00:00Z"
}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
device, err := client.GetDevice("device-123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if device.ID != "device-123" {
t.Errorf("expected ID device-123, got %s", device.ID)
}
if device.SiteID != "site-456" {
t.Errorf("expected SiteID site-456, got %s", device.SiteID)
}
if device.IPAddress != "192.168.1.1" {
t.Errorf("expected IPAddress 192.168.1.1, got %s", device.IPAddress)
}
}
func TestClient_CreateDevice_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/devices" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{
"id": "new-device-id",
"site_id": "site-456",
"ip_address": "192.168.1.100",
"name": "New Device",
"monitoring_enabled": true,
"snmp_enabled": true,
"inserted_at": "2024-01-01T00:00:00Z"
}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
device := Device{
SiteID: "site-456",
IPAddress: "192.168.1.100",
}
created, err := client.CreateDevice(device)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if created.ID != "new-device-id" {
t.Errorf("expected ID new-device-id, got %s", created.ID)
}
}
func TestClient_UpdateDevice_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method)
}
if r.URL.Path != "/api/v1/devices/device-123" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"id": "device-123",
"site_id": "site-456",
"ip_address": "192.168.1.200",
"name": "Updated Device",
"inserted_at": "2024-01-01T00:00:00Z"
}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
device := Device{
SiteID: "site-456",
IPAddress: "192.168.1.200",
}
updated, err := client.UpdateDevice("device-123", device)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if updated.IPAddress != "192.168.1.200" {
t.Errorf("expected IPAddress 192.168.1.200, got %s", updated.IPAddress)
}
}
func TestClient_UpdateDevice_NotFound(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "device not found"}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
device := Device{
SiteID: "site-456",
IPAddress: "192.168.1.200",
}
_, err := client.UpdateDevice("nonexistent", device)
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound, got: %v", err)
}
}
func TestClient_DeleteDevice_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/devices/device-123" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
client := NewClient("test-token", server.URL)
err := client.DeleteDevice("device-123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestClient_APIError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "invalid request"}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
_, err := client.GetDevice("device-123")
if err == nil {
t.Fatal("expected error, got nil")
}
// Should not be ErrNotFound for 400 errors
if errors.Is(err, ErrNotFound) {
t.Error("did not expect ErrNotFound for 400 error")
}
}
func TestClient_ValidationError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnprocessableEntity)
w.Write([]byte(`{"errors": {"ip_address": "is invalid"}}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
device := Device{
SiteID: "site-456",
IPAddress: "invalid",
}
_, err := client.CreateDevice(device)
if err == nil {
t.Fatal("expected error, got nil")
}
if errors.Is(err, ErrNotFound) {
t.Error("did not expect ErrNotFound for validation error")
}
}
func TestClient_GetSite_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/sites/site-123" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"id": "site-123",
"name": "Test Site",
"location": "New York",
"inserted_at": "2024-01-01T00:00:00Z"
}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
site, err := client.GetSite("site-123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if site.ID != "site-123" {
t.Errorf("expected ID site-123, got %s", site.ID)
}
if site.Name != "Test Site" {
t.Errorf("expected Name 'Test Site', got %s", site.Name)
}
}
func TestClient_GetSite_NotFound(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "site not found"}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
_, err := client.GetSite("nonexistent")
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound, got: %v", err)
}
}
func TestNewClient_DefaultBaseURL(t *testing.T) {
client := NewClient("test-token", "")
if client.BaseURL != defaultBaseURL {
t.Errorf("expected default base URL %s, got %s", defaultBaseURL, client.BaseURL)
}
}
func TestNewClient_CustomBaseURL(t *testing.T) {
client := NewClient("test-token", "https://custom.example.com")
if client.BaseURL != "https://custom.example.com" {
t.Errorf("expected custom base URL, got %s", client.BaseURL)
}
}
func TestClient_CreateSite_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/sites" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{
"id": "new-site-id",
"name": "New Site",
"location": "Boston",
"inserted_at": "2024-01-01T00:00:00Z"
}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
site := Site{
Name: "New Site",
}
location := "Boston"
site.Location = &location
created, err := client.CreateSite(site)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if created.ID != "new-site-id" {
t.Errorf("expected ID new-site-id, got %s", created.ID)
}
if created.Name != "New Site" {
t.Errorf("expected Name 'New Site', got %s", created.Name)
}
}
func TestClient_CreateSite_Error(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "name is required"}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
site := Site{}
_, err := client.CreateSite(site)
if err == nil {
t.Fatal("expected error, got nil")
}
}
func TestClient_CreateSite_InvalidJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`invalid json`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
site := Site{Name: "Test"}
_, err := client.CreateSite(site)
if err == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
}
func TestClient_UpdateSite_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method)
}
if r.URL.Path != "/api/v1/sites/site-123" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"id": "site-123",
"name": "Updated Site",
"location": "Chicago",
"inserted_at": "2024-01-01T00:00:00Z"
}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
site := Site{
Name: "Updated Site",
}
updated, err := client.UpdateSite("site-123", site)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if updated.Name != "Updated Site" {
t.Errorf("expected Name 'Updated Site', got %s", updated.Name)
}
}
func TestClient_UpdateSite_NotFound(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "site not found"}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
site := Site{Name: "Test"}
_, err := client.UpdateSite("nonexistent", site)
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound, got: %v", err)
}
}
func TestClient_UpdateSite_InvalidJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`invalid json`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
site := Site{Name: "Test"}
_, err := client.UpdateSite("site-123", site)
if err == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
}
func TestClient_DeleteSite_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/sites/site-123" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
client := NewClient("test-token", server.URL)
err := client.DeleteSite("site-123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestClient_DeleteSite_NotFound(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "site not found"}`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
err := client.DeleteSite("nonexistent")
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound, got: %v", err)
}
}
func TestClient_GetSite_InvalidJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`invalid json`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
_, err := client.GetSite("site-123")
if err == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
}
func TestClient_GetDevice_InvalidJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`invalid json`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
_, err := client.GetDevice("device-123")
if err == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
}
func TestClient_CreateDevice_InvalidJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`invalid json`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
device := Device{SiteID: "site-123", IPAddress: "192.168.1.1"}
_, err := client.CreateDevice(device)
if err == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
}
func TestClient_UpdateDevice_InvalidJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`invalid json`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
device := Device{SiteID: "site-123", IPAddress: "192.168.1.1"}
_, err := client.UpdateDevice("device-123", device)
if err == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
}
func TestClient_ConnectionError(t *testing.T) {
client := NewClient("test-token", "http://localhost:99999")
_, err := client.GetDevice("device-123")
if err == nil {
t.Fatal("expected connection error, got nil")
}
}
func TestClient_APIErrorWithoutJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`Internal Server Error`))
}))
defer server.Close()
client := NewClient("test-token", server.URL)
_, err := client.GetDevice("device-123")
if err == nil {
t.Fatal("expected error, got nil")
}
if errors.Is(err, ErrNotFound) {
t.Error("did not expect ErrNotFound for 500 error")
}
}

View file

@ -2,6 +2,7 @@ package provider
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/path"
@ -205,6 +206,11 @@ func (r *DeviceResource) Read(ctx context.Context, req resource.ReadRequest, res
device, err := r.client.GetDevice(data.ID.ValueString())
if err != nil {
if errors.Is(err, ErrNotFound) {
// Device was deleted outside of Terraform, remove from state
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Failed to read device", err.Error())
return
}
@ -289,6 +295,37 @@ func (r *DeviceResource) Update(ctx context.Context, req resource.UpdateRequest,
updated, err := r.client.UpdateDevice(data.ID.ValueString(), device)
if err != nil {
if errors.Is(err, ErrNotFound) {
// Device was deleted outside of Terraform, recreate it
created, createErr := r.client.CreateDevice(device)
if createErr != nil {
resp.Diagnostics.AddError("Failed to create device (after 404 on update)", createErr.Error())
return
}
data.ID = types.StringValue(created.ID)
data.InsertedAt = types.StringValue(created.InsertedAt)
data.IPAddress = types.StringValue(created.IPAddress)
if created.Name != nil {
data.Name = types.StringValue(*created.Name)
}
if created.Description != nil {
data.Description = types.StringValue(*created.Description)
}
if created.MonitoringEnabled != nil {
data.MonitoringEnabled = types.BoolValue(*created.MonitoringEnabled)
}
if created.SNMPEnabled != nil {
data.SNMPEnabled = types.BoolValue(*created.SNMPEnabled)
}
if created.SNMPVersion != nil {
data.SNMPVersion = types.StringValue(*created.SNMPVersion)
}
if created.SNMPPort != nil {
data.SNMPPort = types.Int64Value(int64(*created.SNMPPort))
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
return
}
resp.Diagnostics.AddError("Failed to update device", err.Error())
return
}

View file

@ -0,0 +1,722 @@
package provider
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"sync"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func TestAccDeviceResource_basic(t *testing.T) {
var deviceID string
var mu sync.Mutex
monitoringEnabled := true
snmpEnabled := true
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/devices":
deviceID = "test-device-id"
name := "Test Device"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "192.168.1.1",
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/devices/"+deviceID:
name := "Test Device"
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "192.168.1.1",
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/devices/"+deviceID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.1"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_device.test", "site_id", "site-123"),
resource.TestCheckResourceAttr("towerops_device.test", "ip_address", "192.168.1.1"),
resource.TestCheckResourceAttrSet("towerops_device.test", "id"),
resource.TestCheckResourceAttrSet("towerops_device.test", "inserted_at"),
),
},
},
})
}
func TestAccDeviceResource_withAllAttributes(t *testing.T) {
var deviceID string
var mu sync.Mutex
monitoringEnabled := true
snmpEnabled := true
snmpVersion := "2c"
snmpPort := 161
description := "Test description"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/devices":
deviceID = "test-device-id"
name := "Full Device"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "10.0.0.1",
Description: &description,
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/devices/"+deviceID:
name := "Full Device"
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "10.0.0.1",
Description: &description,
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/devices/"+deviceID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccDeviceResourceConfigFull(server.URL),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_device.test", "site_id", "site-123"),
resource.TestCheckResourceAttr("towerops_device.test", "name", "Full Device"),
resource.TestCheckResourceAttr("towerops_device.test", "ip_address", "10.0.0.1"),
resource.TestCheckResourceAttr("towerops_device.test", "description", "Test description"),
resource.TestCheckResourceAttr("towerops_device.test", "monitoring_enabled", "true"),
resource.TestCheckResourceAttr("towerops_device.test", "snmp_enabled", "true"),
resource.TestCheckResourceAttr("towerops_device.test", "snmp_version", "2c"),
resource.TestCheckResourceAttr("towerops_device.test", "snmp_port", "161"),
),
},
},
})
}
func TestAccDeviceResource_update(t *testing.T) {
var deviceID string
var currentIP string
var mu sync.Mutex
monitoringEnabled := true
snmpEnabled := true
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/devices":
deviceID = "test-device-id"
currentIP = "192.168.1.1"
name := "Test Device"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: currentIP,
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/devices/"+deviceID:
name := "Test Device"
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: currentIP,
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/devices/"+deviceID:
var body map[string]Device
json.NewDecoder(r.Body).Decode(&body)
currentIP = body["device"].IPAddress
name := "Test Device"
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: currentIP,
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/devices/"+deviceID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.1"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_device.test", "ip_address", "192.168.1.1"),
),
},
{
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.2"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_device.test", "ip_address", "192.168.1.2"),
),
},
},
})
}
func TestAccDeviceResource_recreateOn404(t *testing.T) {
var deviceID string
var deviceDeleted bool
var currentIP string
var mu sync.Mutex
monitoringEnabled := true
snmpEnabled := true
snmpVersion := "2c"
snmpPort := 161
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/devices":
var body map[string]Device
json.NewDecoder(r.Body).Decode(&body)
deviceID = "new-device-id"
deviceDeleted = false
currentIP = body["device"].IPAddress
name := "Auto-discovered Device"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: currentIP,
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/devices/"+deviceID:
if deviceDeleted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "device not found"}`))
return
}
name := "Auto-discovered Device"
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: currentIP,
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/devices/"+deviceID:
if deviceDeleted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "device not found"}`))
return
}
var body map[string]Device
json.NewDecoder(r.Body).Decode(&body)
currentIP = body["device"].IPAddress
name := "Auto-discovered Device"
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: currentIP,
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/devices/"+deviceID:
deviceDeleted = true
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.1"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_device.test", "ip_address", "192.168.1.1"),
),
},
{
PreConfig: func() {
mu.Lock()
deviceDeleted = true
mu.Unlock()
},
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.2"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_device.test", "ip_address", "192.168.1.2"),
resource.TestCheckResourceAttrSet("towerops_device.test", "id"),
),
},
},
})
}
func TestAccDeviceResource_importState(t *testing.T) {
var deviceID string
var mu sync.Mutex
monitoringEnabled := true
snmpEnabled := true
snmpVersion := "2c"
snmpPort := 161
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/devices":
deviceID = "imported-device-id"
name := "Imported Device"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "192.168.1.1",
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/devices/imported-device-id":
name := "Imported Device"
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Device{
ID: "imported-device-id",
SiteID: "site-123",
Name: &name,
IPAddress: "192.168.1.1",
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/devices/imported-device-id":
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.1"),
},
{
ResourceName: "towerops_device.test",
ImportState: true,
ImportStateVerify: true,
},
},
})
}
func testAccDeviceResourceConfig(apiURL, siteID, ipAddress string) string {
return fmt.Sprintf(`
provider "towerops" {
token = "test-token"
api_url = %q
}
resource "towerops_device" "test" {
site_id = %q
ip_address = %q
}
`, apiURL, siteID, ipAddress)
}
func testAccDeviceResourceConfigFull(apiURL string) string {
return fmt.Sprintf(`
provider "towerops" {
token = "test-token"
api_url = %q
}
resource "towerops_device" "test" {
site_id = "site-123"
name = "Full Device"
ip_address = "10.0.0.1"
description = "Test description"
monitoring_enabled = true
snmp_enabled = true
snmp_version = "2c"
snmp_port = 161
}
`, apiURL)
}
func TestAccDeviceResource_createError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost && r.URL.Path == "/api/v1/devices" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "ip_address is required"}`))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccDeviceResourceConfig(server.URL, "site-123", "invalid"),
ExpectError: regexp.MustCompile(`Failed to create device`),
},
},
})
}
func TestAccDeviceResource_updateError(t *testing.T) {
var deviceID string
var mu sync.Mutex
monitoringEnabled := true
snmpEnabled := true
snmpVersion := "2c"
snmpPort := 161
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/devices":
deviceID = "test-device-id"
name := "Test Device"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "192.168.1.1",
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/devices/"+deviceID:
name := "Test Device"
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "192.168.1.1",
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/devices/"+deviceID:
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "update failed"}`))
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/devices/"+deviceID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.1"),
},
{
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.2"),
ExpectError: regexp.MustCompile(`Failed to update device`),
},
},
})
}
func TestAccDeviceResource_deleteError(t *testing.T) {
var deviceID string
var mu sync.Mutex
monitoringEnabled := true
snmpEnabled := true
snmpVersion := "2c"
snmpPort := 161
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/devices":
deviceID = "test-device-id"
name := "Test Device"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "192.168.1.1",
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/devices/"+deviceID:
name := "Test Device"
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "192.168.1.1",
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/devices/"+deviceID:
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "delete failed"}`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.1"),
Destroy: true,
},
},
ErrorCheck: func(err error) error {
if err != nil && regexp.MustCompile(`Failed to delete device`).MatchString(err.Error()) {
return nil
}
return err
},
})
}
func TestAccDeviceResource_recreateOn404_createError(t *testing.T) {
var deviceID string
var deviceDeleted bool
var createCount int
var mu sync.Mutex
monitoringEnabled := true
snmpEnabled := true
snmpVersion := "2c"
snmpPort := 161
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/devices":
createCount++
if createCount > 1 {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "create failed after 404"}`))
return
}
deviceID = "test-device-id"
deviceDeleted = false
name := "Test Device"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "192.168.1.1",
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/devices/"+deviceID:
if deviceDeleted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "device not found"}`))
return
}
name := "Test Device"
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "192.168.1.1",
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/devices/"+deviceID:
if deviceDeleted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "device not found"}`))
return
}
name := "Test Device"
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Device{
ID: deviceID,
SiteID: "site-123",
Name: &name,
IPAddress: "192.168.1.2",
MonitoringEnabled: &monitoringEnabled,
SNMPEnabled: &snmpEnabled,
SNMPVersion: &snmpVersion,
SNMPPort: &snmpPort,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/devices/"+deviceID:
deviceDeleted = true
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.1"),
},
{
PreConfig: func() {
mu.Lock()
deviceDeleted = true
mu.Unlock()
},
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.2"),
ExpectError: regexp.MustCompile(`Failed to create device \(after 404 on update\)`),
},
},
})
}

View file

@ -0,0 +1,95 @@
package provider
import (
"net/http"
"net/http/httptest"
"os"
"regexp"
"testing"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func testAccProtoV6ProviderFactories(serverURL string) map[string]func() (tfprotov6.ProviderServer, error) {
// Set the API URL for tests
os.Setenv("TOWEROPS_TEST_API_URL", serverURL)
return map[string]func() (tfprotov6.ProviderServer, error){
"towerops": providerserver.NewProtocol6WithError(New("test")()),
}
}
func TestProvider_Schema(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{}`))
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccProviderConfig(server.URL),
},
},
})
}
func TestProvider_MissingToken(t *testing.T) {
t.Parallel()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(""),
Steps: []resource.TestStep{
{
Config: `
provider "towerops" {
api_url = "http://localhost"
}
resource "towerops_site" "test" {
name = "Test"
}
`,
ExpectError: regexp.MustCompile(`Missing TowerOps API Token`),
},
},
})
}
func TestProvider_EmptyToken(t *testing.T) {
t.Parallel()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(""),
Steps: []resource.TestStep{
{
Config: `
provider "towerops" {
token = ""
api_url = "http://localhost"
}
resource "towerops_site" "test" {
name = "Test"
}
`,
ExpectError: regexp.MustCompile(`Missing TowerOps API Token`),
},
},
})
}
func testAccProviderConfig(apiURL string) string {
return `
provider "towerops" {
token = "test-token"
api_url = "` + apiURL + `"
}
`
}

View file

@ -0,0 +1,507 @@
package provider
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"sync"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func TestAccSiteResource_basic(t *testing.T) {
var siteID string
var mu sync.Mutex
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites":
siteID = "test-site-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Test Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Test Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfig(server.URL, "Test Site"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_site.test", "name", "Test Site"),
resource.TestCheckResourceAttrSet("towerops_site.test", "id"),
resource.TestCheckResourceAttrSet("towerops_site.test", "inserted_at"),
),
},
},
})
}
func TestAccSiteResource_withLocation(t *testing.T) {
var siteID string
var mu sync.Mutex
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
location := "New York, NY"
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites":
siteID = "test-site-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Site With Location",
Location: &location,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Site With Location",
Location: &location,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfigWithLocation(server.URL, "Site With Location", "New York, NY"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_site.test", "name", "Site With Location"),
resource.TestCheckResourceAttr("towerops_site.test", "location", "New York, NY"),
),
},
},
})
}
func TestAccSiteResource_update(t *testing.T) {
var siteID string
var currentName string
var mu sync.Mutex
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites":
siteID = "test-site-id"
currentName = "Original Name"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: currentName,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: currentName,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/sites/"+siteID:
var body map[string]Site
json.NewDecoder(r.Body).Decode(&body)
currentName = body["site"].Name
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: currentName,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfig(server.URL, "Original Name"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_site.test", "name", "Original Name"),
),
},
{
Config: testAccSiteResourceConfig(server.URL, "Updated Name"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_site.test", "name", "Updated Name"),
),
},
},
})
}
func TestAccSiteResource_importState(t *testing.T) {
var siteID string
var mu sync.Mutex
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites":
siteID = "imported-site-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Imported Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/sites/imported-site-id":
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: "imported-site-id",
Name: "Imported Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/sites/imported-site-id":
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfig(server.URL, "Imported Site"),
},
{
ResourceName: "towerops_site.test",
ImportState: true,
ImportStateVerify: true,
},
},
})
}
func testAccSiteResourceConfig(apiURL, name string) string {
return fmt.Sprintf(`
provider "towerops" {
token = "test-token"
api_url = %q
}
resource "towerops_site" "test" {
name = %q
}
`, apiURL, name)
}
func testAccSiteResourceConfigWithLocation(apiURL, name, location string) string {
return fmt.Sprintf(`
provider "towerops" {
token = "test-token"
api_url = %q
}
resource "towerops_site" "test" {
name = %q
location = %q
}
`, apiURL, name, location)
}
func TestAccSiteResource_createError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "name is required"}`))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfig(server.URL, ""),
ExpectError: regexp.MustCompile(`Failed to create site`),
},
},
})
}
func TestAccSiteResource_readError(t *testing.T) {
var siteID string
var mu sync.Mutex
readCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites":
siteID = "test-site-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Test Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/sites/"+siteID:
readCount++
if readCount > 1 {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "internal server error"}`))
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Test Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfig(server.URL, "Test Site"),
ExpectError: regexp.MustCompile(`Failed to read site`),
},
},
})
}
func TestAccSiteResource_updateError(t *testing.T) {
var siteID string
var mu sync.Mutex
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites":
siteID = "test-site-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Original Name",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Original Name",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "update failed"}`))
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfig(server.URL, "Original Name"),
},
{
Config: testAccSiteResourceConfig(server.URL, "Updated Name"),
ExpectError: regexp.MustCompile(`Failed to update site`),
},
},
})
}
func TestAccSiteResource_deleteError(t *testing.T) {
var siteID string
var mu sync.Mutex
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites":
siteID = "test-site-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Test Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Test Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "delete failed"}`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfig(server.URL, "Test Site"),
Destroy: true,
},
},
ErrorCheck: func(err error) error {
if err != nil && regexp.MustCompile(`Failed to delete site`).MatchString(err.Error()) {
return nil
}
return err
},
})
}
func TestAccSiteResource_withSNMPCommunity(t *testing.T) {
var siteID string
var mu sync.Mutex
community := "public"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites":
siteID = "test-site-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "SNMP Site",
SNMPCommunity: &community,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "SNMP Site",
SNMPCommunity: &community,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfigWithSNMPCommunity(server.URL, "SNMP Site", "public"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_site.test", "name", "SNMP Site"),
resource.TestCheckResourceAttr("towerops_site.test", "snmp_community", "public"),
),
},
},
})
}
func testAccSiteResourceConfigWithSNMPCommunity(apiURL, name, community string) string {
return fmt.Sprintf(`
provider "towerops" {
token = "test-token"
api_url = %q
}
resource "towerops_site" "test" {
name = %q
snmp_community = %q
}
`, apiURL, name, community)
}