diff --git a/.gitignore b/.gitignore index 11937fe..4fd6b9a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ CLAUDE.md terraform-provider-towerops dist/ +coverage.out diff --git a/go.mod b/go.mod index 2f1a2de..f427227 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index b24cac7..9067c4e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/provider/client.go b/internal/provider/client.go index e439f2c..d1a0b19 100644 --- a/internal/provider/client.go +++ b/internal/provider/client.go @@ -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 != "" { diff --git a/internal/provider/client_test.go b/internal/provider/client_test.go new file mode 100644 index 0000000..bcab6db --- /dev/null +++ b/internal/provider/client_test.go @@ -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") + } +} diff --git a/internal/provider/device_resource.go b/internal/provider/device_resource.go index cc6edb7..cd5c44a 100644 --- a/internal/provider/device_resource.go +++ b/internal/provider/device_resource.go @@ -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 } diff --git a/internal/provider/device_resource_test.go b/internal/provider/device_resource_test.go new file mode 100644 index 0000000..ef9187a --- /dev/null +++ b/internal/provider/device_resource_test.go @@ -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\)`), + }, + }, + }) +} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go new file mode 100644 index 0000000..393028d --- /dev/null +++ b/internal/provider/provider_test.go @@ -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 + `" +} +` +} diff --git a/internal/provider/site_resource_test.go b/internal/provider/site_resource_test.go new file mode 100644 index 0000000..f237f54 --- /dev/null +++ b/internal/provider/site_resource_test.go @@ -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) +}