Compare commits

..
2 Commits
26 changed files with 1362 additions and 1242 deletions

No files matched your search

+34 -34
View File
@@ -1,56 +1,56 @@
module github.com/hashicorp/boundary-plugin-aws
go 1.24.0
go 1.25.7
require (
github.com/aws/aws-sdk-go-v2 v1.41.5
github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0
github.com/aws/aws-sdk-go-v2/service/iam v1.52.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2
github.com/aws/smithy-go v1.24.2
github.com/aws/aws-sdk-go-v2 v1.41.7
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5
github.com/aws/smithy-go v1.25.1
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/hashicorp/boundary/sdk v0.0.51
github.com/hashicorp/boundary/sdk v0.0.59
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.2
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0
github.com/hashicorp/terraform-json v0.23.0
github.com/mitchellh/mapstructure v1.5.0
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.10
google.golang.org/protobuf v1.36.11
)
require (
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.2 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/eventlogger v0.2.6-0.20231025104552-802587e608f0 // indirect
github.com/hashicorp/eventlogger v0.2.11 // indirect
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20231025104552-802587e608f0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-kms-wrapping/v2 v2.0.16 // indirect
github.com/hashicorp/go-kms-wrapping/v2 v2.0.20 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
@@ -59,11 +59,11 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/zclconf/go-cty v1.15.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+104 -132
View File
@@ -8,48 +8,68 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk=
github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4=
github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0 h1:RhSoBFT5/8tTmIseJUXM6INTXTQDF8+0oyxWBnozIms=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0/go.mod h1:mzj8EEjIHSN2oZRXiw1Dd+uB4HZTl7hC8nBzX9IZMWw=
github.com/aws/aws-sdk-go-v2/service/iam v1.52.2 h1:li0ooCUfHIivHn8nB3LstP6HgdNefwu5gnXE4MLVz/U=
github.com/aws/aws-sdk-go-v2/service/iam v1.52.2/go.mod h1:PuHz5kGh1jtsNpjezdYhRp7xgn6DzCNJJfQt7O7U9Aw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE=
github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY=
github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0=
github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o=
github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.8 h1:1/bT9kDdLQzfZ1e6J6hpW+SfNDd6xrV8F3M2CuGyUz8=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.8/go.mod h1:RbdwTONAIi59ej/+1H+QzZORt5bcyAtbrS7FQb2pvz0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0 h1:fTLR6dLDTGChAjecRPlVrKeznT0rVdzR4yn9Z68MTGk=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0/go.mod h1:V0jbRy1/IPapnkqgXSwVOFB+u5pnCwd9S+R3pKWULC4=
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6 h1:EWehQXACWr+6hzfZPwZChlfoVhiUCfLHE0Xh3kAfzWQ=
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6/go.mod h1:qRXgEBWPIltrWHQwU+HkyBvwh1QgeigFcaCGCIVrWk0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.8 h1:tIN8MFT1z5STK5kTdOT1TCfMN/bn5fSEnlKsTL8qBOU=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.8/go.mod h1:VKS56txtNWjKI8FqD/hliL0BcshyF4ZaLBa1rm2Y+5s=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.8 h1:AgYCo1Rb8XChJXA871BXHDNxNWOTAr6V5YdsRIBbgv0=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.8/go.mod h1:Au9dvIGm1Hbqnt29d3VakOCQuN9l0WrkDDTRq8biWS4=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.2 h1:T7b3qniouutV5Wwa9B1q7gW+Y8s1B3g9RE9qa7zLBIM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.2/go.mod h1:tW9TsLb6t1eaTdBE6LITyJW1m/+DjQPU78Q/jT2FJu8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -65,60 +85,57 @@ 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.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
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.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/boundary/sdk v0.0.51 h1:a7XpHNTYaeBXRhJkc8MC0siPtfJELYH/ibjw/0UbJEg=
github.com/hashicorp/boundary/sdk v0.0.51/go.mod h1:gSrMTppD30IPVsUqs4SwQowu+OlR5/+58draey++SkQ=
github.com/hashicorp/boundary/sdk v0.0.59 h1:XNniO0rwgihLdF1+4qDHKN3gdta3AU1JteZResCJ8Oc=
github.com/hashicorp/boundary/sdk v0.0.59/go.mod h1:VYwVGAEaHTzihRXpzKZL6EhWBhll59Iul3PGLVL63ao=
github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU=
github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/eventlogger v0.2.6-0.20231025104552-802587e608f0 h1:f9oX8/3zxiQrfrWnBeyjDm4S02GAU02OBtCRoZOUwlo=
github.com/hashicorp/eventlogger v0.2.6-0.20231025104552-802587e608f0/go.mod h1://CHt6/j+Q2lc0NlUB5af4aS2M0c0aVBg9/JfcpAyhM=
github.com/hashicorp/eventlogger v0.2.11 h1:lkK8ARM/DrCeL5deH2yVS6p40337dESi+77p9pIfLqo=
github.com/hashicorp/eventlogger v0.2.11/go.mod h1:Rmc3MEopz7jUnLokZVTWAsXaEP5rqd20ObGS+pcau3U=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20231025104552-802587e608f0 h1:iAb287bq0TaWTnhDYuN/zVqdD2EwanQg9ncVelC60Xc=
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20231025104552-802587e608f0/go.mod h1:tMywUTIvdB/FXhwm6HMTt61C8/eODY6gitCHhXtyojg=
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-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-kms-wrapping/plugin/v2 v2.0.7 h1:gM4OwbF16Cmfxt2QMkoGMQbRTfYFZLvDMPgU3rM3KIo=
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.7/go.mod h1:7ZMHVluyqgHgEuTADeDzFNWoA9mnyPfdiK8Tk2Bct1c=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.16 h1:WZeXfD26QMWYC35at25KgE021SF9L3u9UMHK8fJAdV0=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.16/go.mod h1:ZiKZctjRTLEppuRwrttWkp71VYMbTTCkazK4xT7U/NQ=
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.8 h1:/GIUjn9GkFXMk/8/irRdbdtmx8CcyeyWdVy/E5LvzyA=
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.8/go.mod h1:JDc9UOD4EVRDIwPVethJcT5Ibi/Nas6eQDPtA60iwP0=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.20 h1:afreZ1WJd0WI7v4NsMZ1aL7V/T59sxPuKmQDgGUja20=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.20/go.mod h1:NeK2Ul15t1zutp/dZzt28XQrGZHosbxE/QLNNfaWObM=
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.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg=
github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0=
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-secure-stdlib/awsutil/v2 v2.1.2 h1:KlDrZWx/wYpnUcFPB0s9uhPjyNZoeIfhrV2MN/Ly76s=
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.2/go.mod h1:6+rVulOPNCQbL3Xv2iLCqM0JmU2WO2wRzP1C6hBKeB8=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.11 h1:uPW2Wn0YlmI9RGSkZpcIplnVRwJ7BCiGpk1vnF2TMw4=
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.11/go.mod h1:uis9dCmOzXuOaRyXq+1Foh31kcvXKoWogjNnhfjHfW8=
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.9 h1:0S0ctJ7Ra8O7ap+/3fZUnzJ3VzJyirWS/WnNCuOYtZY=
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.9/go.mod h1:TNNdgtjLgVDbrgFcyCKrlAicIl3dZF94swJltyGUX2M=
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.13 h1:TayxZ5drfMP0G6T++WvnLESGLOWeHtdWDWWTAi2e3Qk=
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.13/go.mod h1:NDRQ/F3DXTylqjORAP0cA+puH/JFrLlT+NlqxHK2/e8=
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.10 h1:2iDz+t0JLl1W0tJhvmhsh/UBgT1JgC8Qxz8HxYMWXQo=
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.10/go.mod h1:eZkXE+osawMrAWR4wJRmyKauUwH6mNGbjFuiDujnbPk=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.6 h1:ZYv2XA+tEfFXIToR2jmBgVqQU9gERt0APbWqmUoNGnY=
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.6/go.mod h1:ggFN8dlaLWS2R1gymBbCrvXM/bkZP7hEAa4seqDwhyg=
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.8 h1:pgSicufBI+MvaIG9Keykb3k9B3sWtDNjmsoyrl2/8Qw=
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.8/go.mod h1:sBcjk+paCXCMR9HHLcrYSfPz2FsskD8MQuotKiyM2aA=
github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1 h1:SMGUnbpAcat8rIKHkBPjfv81yC46a8eCNZ2hsR2l1EI=
github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1/go.mod h1:Ch/bf00Qnx77MZd49JRgHYqHQjtEmTgGU2faufpVZb0=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.3 h1:xbrxd0U9XQW8qL1BAz2XrAjAF/P2vcqUTAues9c24B8=
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.3/go.mod h1:LWq2Sy8UoKKuK4lFuCNWSjJj57MhNNf2zzBWMtkAIX4=
github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I=
github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
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/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
@@ -127,20 +144,16 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI=
github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
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/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f h1:E87tDTVS5W65euzixn7clSzK66puSt1H4I5SC0EmHH4=
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f/go.mod h1:3J2qVK16Lq8V+wfiL2lPeDZ7UWMxk5LemerHa1p6N00=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -151,8 +164,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng=
github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4=
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/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
@@ -168,7 +179,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
@@ -178,12 +188,9 @@ github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ=
github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
@@ -198,69 +205,34 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48=
+1 -1
View File
@@ -130,7 +130,7 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
cfg, err := awsutil.NewCredentialsConfig(opts...)
if err != nil {
return nil, errors.BadRequestStatus("%s", err.Error())
return nil, errors.BadRequestStatus(err.Error())
}
return cfg, nil
+73 -10
View File
@@ -16,6 +16,7 @@ import (
"github.com/aws/aws-sdk-go-v2/aws/retry"
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -52,6 +53,10 @@ const (
// whose key does not exist
awsErrorNoSuchKey = "NoSuchKey"
// NotFound is returned when attempting to interact with an S3 object
// whose key does not exist
awsErrorNotFound = "NotFound"
// The InvalidObjectState error is returned when trying to
// access an object that was moved into cold storage.
awsErrorInvalidObjectState = "InvalidObjectState"
@@ -73,7 +78,7 @@ func InvalidArgumentError(msg string, f map[string]string) error {
sort.Strings(fieldMsgs)
msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", "))
}
return BadRequestStatus("%s", msg)
return BadRequestStatus(msg)
}
// ParseAWSError converts an aws service error into a RPC status. This method
@@ -118,7 +123,50 @@ func ParseAWSError(op string, err error) (st *status.Status, permission *pb.Perm
return status.New(codes.Unavailable, statusMsg), nil
}
// parse some specific aws error codes that we have special cred states for
// match error type against NotFound
var notFoundErr *types.NotFound
if errors.As(err, &notFoundErr) {
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
return status.New(codes.NotFound, statusMsg), nil
}
// match error type against NoSuchkey
var noSuchKeyErr *types.NoSuchKey
if errors.As(err, &noSuchKeyErr) {
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
return status.New(codes.NotFound, statusMsg), nil
}
// match error type against InvalidObjectState
var invalidObjectStateErr *types.InvalidObjectState
if errors.As(err, &invalidObjectStateErr) {
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
return status.New(codes.NotFound, statusMsg), nil
}
// match error type against NoSuchBucket
var noSuchBucketErr *types.NoSuchBucket
if errors.As(err, &noSuchBucketErr) {
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
return status.New(codes.NotFound, statusMsg), &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: noSuchBucketErr.ErrorMessage(),
CheckedAt: timestamppb.Now(),
}
}
// match error type against AccessDenied
var accessDeniedErr *types.AccessDenied
if errors.As(err, &accessDeniedErr) {
statusMsg := fmt.Sprintf("aws service %s: invalid credentials: %s", serviceName, msg)
return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: accessDeniedErr.ErrorMessage(),
CheckedAt: timestamppb.Now(),
}
}
// fallback to evaluating the smithy APIError code
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
switch apiErr.ErrorCode() {
@@ -145,6 +193,8 @@ func ParseAWSError(op string, err error) (st *status.Status, permission *pb.Perm
return status.New(codes.Aborted, statusMsg), nil
case awsErrorNoSuchKey:
fallthrough
case awsErrorNotFound:
fallthrough
case awsErrorInvalidObjectState:
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
return status.New(codes.NotFound, statusMsg), nil
@@ -156,8 +206,9 @@ func ParseAWSError(op string, err error) (st *status.Status, permission *pb.Perm
}
}
// default to evaluating the http status code
if httpErr, ok := err.(*awshttp.ResponseError); ok {
// fallback to evaluating the http status code
var httpErr *awshttp.ResponseError
if errors.As(err, &httpErr) {
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(httpErr.Response.Body)
if err != nil {
@@ -206,14 +257,26 @@ func ParseAWSError(op string, err error) (st *status.Status, permission *pb.Perm
return st, nil
}
// BadRequestStatus returns a status error with an invalid
// argument code
func BadRequestStatus(format string, args ...any) error {
// BadRequestStatusf returns a status error with an invalid
// argument code and a formatted error message.
func BadRequestStatusf(format string, args ...any) error {
return status.New(codes.InvalidArgument, fmt.Sprintf(format, args...)).Err()
}
// UnknownStatus returns a status error with an internal
// error code
func UnknownStatus(format string, args ...any) error {
// BadRequestStatus returns a status error with an invalid
// argument code and the provided message.
func BadRequestStatus(msg string) error {
return status.New(codes.InvalidArgument, msg).Err()
}
// UnknownStatusf returns a status error with an internal
// error code and a formatted error message.
func UnknownStatusf(format string, args ...any) error {
return status.New(codes.Internal, fmt.Sprintf(format, args...)).Err()
}
// UnknownStatus returns a status error with an internal
// error code and the provided message.
func UnknownStatus(msg string) error {
return status.New(codes.Internal, msg).Err()
}
+82 -4
View File
@@ -134,6 +134,17 @@ func Test_ParseAWSError(t *testing.T) {
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "wrapped-http-status-code-forbidden",
err: fmt.Errorf("wrap: %w", TestAwsHttpResponseError(http.StatusForbidden, "forbidden request")),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "forbidden request",
CheckedAt: timestamppb.Now(),
},
},
{
name: "aws-throttling",
err: TestAwsError("Throttling", "throttling error"),
@@ -311,6 +322,55 @@ func Test_ParseAWSError(t *testing.T) {
CheckedAt: timestamppb.Now(),
},
},
{
name: "aws-s3-typed-not-found",
err: TestAwsS3NotFoundError("object was not found"),
expectedStatusCode: codes.NotFound,
expectedStatusMsg: "aws service unknown: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-s3-typed-no-such-key",
err: TestAwsS3NoSuchKeyError("key does not exist"),
expectedStatusCode: codes.NotFound,
expectedStatusMsg: "aws service unknown: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-s3-typed-invalid-object-state",
err: TestAwsS3InvalidObjectStateError("object is archived"),
expectedStatusCode: codes.NotFound,
expectedStatusMsg: "aws service unknown: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
name: "aws-s3-typed-no-such-bucket",
err: TestAwsS3NoSuchBucketError("bucket does not exist"),
expectedStatusCode: codes.NotFound,
expectedStatusMsg: "aws service unknown: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "bucket does not exist",
CheckedAt: timestamppb.Now(),
},
},
{
name: "aws-s3-typed-access-denied",
err: TestAwsS3AccessDeniedError("access denied"),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials: test",
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "access denied",
CheckedAt: timestamppb.Now(),
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
@@ -330,18 +390,36 @@ func Test_ParseAWSError(t *testing.T) {
}
}
func Test_BadRequestStatus(t *testing.T) {
func Test_BadRequestStatusf(t *testing.T) {
require, assert := require.New(t), assert.New(t)
err := BadRequestStatus("test: %s", "hello world")
err := BadRequestStatusf("test: %s", "hello world")
assert.ErrorContains(err, "test: hello world")
assert.Equal(status.Code(err), codes.InvalidArgument)
_, ok := status.FromError(err)
require.True(ok)
}
func Test_UnknownStatus(t *testing.T) {
func Test_BadRequestStatus(t *testing.T) {
require, assert := require.New(t), assert.New(t)
err := UnknownStatus("test: %s", "hello world")
err := BadRequestStatus("test: hello world")
assert.ErrorContains(err, "test: hello world")
assert.Equal(status.Code(err), codes.InvalidArgument)
_, ok := status.FromError(err)
require.True(ok)
}
func Test_UnknownStatusf(t *testing.T) {
require, assert := require.New(t), assert.New(t)
err := UnknownStatusf("test: %s", "hello world")
assert.ErrorContains(err, "test: hello world")
assert.Equal(status.Code(err), codes.Internal)
_, ok := status.FromError(err)
require.True(ok)
}
func Test_UnknownStatus(t *testing.T) {
require, assert := require.New(t), assert.New(t)
err := UnknownStatusf("test: hello world")
assert.ErrorContains(err, "test: hello world")
assert.Equal(status.Code(err), codes.Internal)
_, ok := status.FromError(err)
+26
View File
@@ -9,6 +9,7 @@ import (
"strings"
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
smithyhttp "github.com/aws/smithy-go/transport/http"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
@@ -39,6 +40,31 @@ func TestAwsError(code, msg string) error {
}
}
// TestAwsS3NotFoundError returns an S3 NotFound error.
func TestAwsS3NotFoundError(msg string) error {
return &types.NotFound{Message: &msg}
}
// TestAwsS3NoSuchKeyError returns an S3 NoSuchKey error.
func TestAwsS3NoSuchKeyError(msg string) error {
return &types.NoSuchKey{Message: &msg}
}
// TestAwsS3InvalidObjectStateError returns an S3 InvalidObjectState error.
func TestAwsS3InvalidObjectStateError(msg string) error {
return &types.InvalidObjectState{Message: &msg}
}
// TestAwsS3NoSuchBucketError returns an S3 NoSuchBucket error.
func TestAwsS3NoSuchBucketError(msg string) error {
return &types.NoSuchBucket{Message: &msg}
}
// TestAwsS3AccessDeniedError returns an S3 AccessDenied error.
func TestAwsS3AccessDeniedError(msg string) error {
return &types.AccessDenied{Message: &msg}
}
func CheckSimilarPermission(assert *assert.Assertions, expected, actual *pb.Permission, substituteNilWithDefaultOk bool) {
if expected == nil && substituteNilWithDefaultOk {
expected = &pb.Permission{
+1 -40
View File
@@ -16,36 +16,6 @@ boundary host-catalogs create plugin \
-secret secret_access_key='SECRET'
```
To create a host catalog that returns addresses only from the primary network
interface:
```
boundary host-catalogs create plugin \
-scope-id p_1234567890 \
-name "Example Plugin-Based Host Catalog" \
-description "Description for plugin-based host catalog" \
-plugin-name aws \
-attr region=REGION \
-attr primary_interface_only=true \
-secret access_key_id='KEY' \
-secret secret_access_key='SECRET'
```
To create a host catalog that excludes public IPv4 addresses while keeping
private IPv4 addresses:
```
boundary host-catalogs create plugin \
-scope-id p_1234567890 \
-name "Example Plugin-Based Host Catalog" \
-description "Description for plugin-based host catalog" \
-plugin-name aws \
-attr region=REGION \
-attr exclude_public_ips=true \
-secret access_key_id='KEY' \
-secret secret_access_key='SECRET'
```
To create a host set, filtering the host set based on tag keys `foo` or `bar`
(either tag can be present), ensuring that any targets set to this host set only
connect to external addresses in the `54.0.0.0/8` class A subnet:
@@ -105,6 +75,7 @@ a configured IAM user for this provider:
"Resource": "*"
}
]
}
```
* `iam:GetUser`, `iam:CreateAccessKey`, and `iam:DeleteAccessKey`, configured to
@@ -145,16 +116,6 @@ The following `attributes` are valid on an AWS host catalog resource:
provider.
- `role_tags` (object): The key-value pair tags configured for the `AssumeRole`
provider.
- `primary_interface_only` (bool): If `true`, only addresses from the primary
ENI are returned. When `false` or omitted, addresses from all attached ENIs
are considered.
- `exclude_private_ips` (bool): If `true`, private IPv4 addresses are omitted.
- `exclude_public_ips` (bool): If `true`, public IPv4 addresses are omitted.
If none of the exclusion flags are set, the plugin preserves the historical
behavior and returns all discovered address classes. The exclusion flags apply
to both IPv4 and IPv6 addresses based on whether the IP is classified as public
or private.
The following `secrets` are required on an AWS host catalog resource:
-46
View File
@@ -22,18 +22,6 @@ type CatalogAttributes struct {
// DualStack is used for configuring how the aws client will resolve requests.
DualStack bool
// PrimaryInterfaceOnly restricts discovery to the instance's primary ENI.
PrimaryInterfaceOnly bool
// ExcludePrivateIps controls whether private IPv4 addresses are omitted.
ExcludePrivateIps bool
// ExcludePublicIps controls whether public IPv4 addresses are omitted.
ExcludePublicIps bool
// ExcludeIpv6 controls whether IPv6 addresses are omitted.
ExcludeIpv6 bool
}
func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
@@ -52,36 +40,6 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
}
delete(unknownFields, ConstAwsDualStack)
primaryInterfaceOnly, err := values.GetBoolValue(in, ConstPrimaryInterfaceOnly, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstPrimaryInterfaceOnly)] = err.Error()
}
delete(unknownFields, ConstPrimaryInterfaceOnly)
excludePrivateIPs, err := values.GetBoolValue(in, ConstExcludePrivateIps, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstExcludePrivateIps)] = err.Error()
}
delete(unknownFields, ConstExcludePrivateIps)
excludePublicIPs, err := values.GetBoolValue(in, ConstExcludePublicIps, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstExcludePublicIps)] = err.Error()
}
delete(unknownFields, ConstExcludePublicIps)
excludeIpv6, err := values.GetBoolValue(in, ConstExcludeIpv6, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstExcludeIpv6)] = err.Error()
}
delete(unknownFields, ConstExcludeIpv6)
if excludePrivateIPs && excludePublicIPs && excludeIpv6 {
badFields[fmt.Sprintf("attributes.%s", ConstExcludePrivateIps)] = "cannot be combined with exclude_public_ips and exclude_ipv6"
badFields[fmt.Sprintf("attributes.%s", ConstExcludePublicIps)] = "cannot be combined with exclude_private_ips and exclude_ipv6"
badFields[fmt.Sprintf("attributes.%s", ConstExcludeIpv6)] = "cannot be combined with exclude_private_ips and exclude_public_ips"
}
for s := range unknownFields {
switch s {
// Ignore knownFields from CredentialAttributes
@@ -109,10 +67,6 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
return &CatalogAttributes{
CredentialAttributes: credAttributes,
DualStack: dualStack,
PrimaryInterfaceOnly: primaryInterfaceOnly,
ExcludePrivateIps: excludePrivateIPs,
ExcludePublicIps: excludePublicIPs,
ExcludeIpv6: excludeIpv6,
}, nil
}
-100
View File
@@ -59,106 +59,6 @@ func TestGetCatalogAttributes(t *testing.T) {
DualStack: true,
},
},
{
name: "with primary interface only",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"primary_interface_only": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
PrimaryInterfaceOnly: true,
},
},
{
name: "with exclude public ip",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_public_ips": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePublicIps: true,
},
},
{
name: "with exclude private ip",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePrivateIps: true,
},
},
{
name: "with mixed attributes",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"dual_stack": structpb.NewBoolValue(true),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
"primary_interface_only": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
DualStack: true,
ExcludePrivateIps: true,
ExcludeIpv6: true,
PrimaryInterfaceOnly: true,
},
},
{
name: "can exclude public and private IPs when IPv6 remains enabled",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_public_ips": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePrivateIps: true,
ExcludePublicIps: true,
},
},
{
name: "cannot exclude public private and IPv6 together",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_public_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expectedErrContains: "attributes.exclude_ipv6: cannot be combined with exclude_private_ips and exclude_public_ips, attributes.exclude_private_ips: cannot be combined with exclude_public_ips and exclude_ipv6, attributes.exclude_public_ips: cannot be combined with exclude_private_ips and exclude_ipv6",
},
{
name: "default",
in: &structpb.Struct{
-12
View File
@@ -13,16 +13,4 @@ const (
// ConstAwsDualStack is the key for the dualstack flag in the aws s3 client.
ConstAwsDualStack = "dual_stack"
// ConstPrimaryInterfaceOnly limits host discovery to the primary ENI.
ConstPrimaryInterfaceOnly = "primary_interface_only"
// ConstExcludePrivateIps excludes private IPv4 addresses.
ConstExcludePrivateIps = "exclude_private_ips"
// ConstExcludePublicIps excludes public IPv4 addresses associated to ENI IPs.
ConstExcludePublicIps = "exclude_public_ips"
// ConstExcludeIpv6 excludes IPv6 addresses.
ConstExcludeIpv6 = "exclude_ipv6"
)
+35 -64
View File
@@ -1,8 +1,8 @@
package host
// Copyright IBM Corp. 2021, 2026
// SPDX-License-Identifier: MPL-2.0
package host
import (
"context"
"fmt"
@@ -63,7 +63,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
// Try to rotate AWS static credentials
@@ -79,7 +79,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
// perform dry run to ensure we can interact with AWS as expected.
@@ -93,7 +93,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
persistedProto, err := catalogState.toProto()
if err != nil {
return nil, errors.BadRequestStatus("%s", err.Error())
return nil, errors.BadRequestStatusf("error converting persisted state to proto: %s", err)
}
return &pb.OnCreateCatalogResponse{
@@ -138,7 +138,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
oldCatalogAttributes.DualStack,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
// Verify the incoming credentials are valid and return any errors to the
@@ -161,7 +161,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up new credential persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up new credential persisted state: %s", err)
}
newCatalogState, err := newAwsCatalogPersistedState(
append([]awsCatalogPersistedStateOption{
@@ -169,7 +169,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
opts := []ec2Option{}
if newCatalogAttributes.DualStack {
@@ -213,7 +213,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
// perform dry run to ensure we can interact with AWS as expected.
@@ -227,7 +227,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
persistedProto, err := catalogState.toProto()
if err != nil {
return nil, errors.BadRequestStatus("%s", err.Error())
return nil, errors.BadRequestStatusf("error converting persisted state to proto: %s", err)
}
return &pb.OnUpdateCatalogResponse{
@@ -264,7 +264,7 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
catalogAttributes.DualStack,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
_, err = newAwsCatalogPersistedState(
@@ -273,7 +273,7 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
// try to delete static credentials
@@ -341,7 +341,7 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
catalogAttributes.DualStack,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
catalogState, err := newAwsCatalogPersistedState(
@@ -350,7 +350,7 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
set := req.GetSet()
@@ -368,7 +368,7 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
describeInstanceFilters, err := buildFilters(setAttrs)
if err != nil {
return nil, errors.BadRequestStatus("error building set filters: %s", err)
return nil, errors.BadRequestStatusf("error building set filters: %s", err)
}
opts := []ec2Option{}
@@ -404,7 +404,7 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
catalogAttributes.DualStack,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
catalogState, err := newAwsCatalogPersistedState(
@@ -413,7 +413,7 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
// As with catalog, we don't need to really look at the old host
@@ -433,7 +433,7 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
describeInstanceFilters, err := buildFilters(setAttrs)
if err != nil {
return nil, errors.BadRequestStatus("error building set filters: %s", err)
return nil, errors.BadRequestStatusf("error building set filters: %s", err)
}
opts := []ec2Option{}
@@ -477,7 +477,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
catalogAttributes.DualStack,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
catalogState, err := newAwsCatalogPersistedState(
@@ -486,7 +486,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
sets := req.GetSets()
@@ -510,7 +510,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
}
if set.GetAttributes() == nil {
return nil, errors.BadRequestStatus("set %s attributes are required", set.GetId())
return nil, errors.BadRequestStatusf("set %s attributes are required", set.GetId())
}
setAttrs, err := getSetAttributes(set.GetAttributes())
if err != nil {
@@ -519,7 +519,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
input, err := buildDescribeInstancesInput(setAttrs, false)
if err != nil {
return nil, errors.BadRequestStatus("error building DescribeInstances parameters for host set id %q: %s", set.GetId(), err)
return nil, errors.BadRequestStatusf("error building DescribeInstances parameters for host set id %q: %s", set.GetId(), err)
}
queries[i] = hostSetQuery{
Id: set.GetId(),
@@ -533,7 +533,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
}
ec2Client, err := catalogState.EC2Client(ctx, opts...)
if err != nil {
return nil, errors.BadRequestStatus("error getting EC2 client: %s", err)
return nil, errors.BadRequestStatusf("error getting EC2 client: %s", err)
}
// Run all queries now and assemble output.
@@ -541,7 +541,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
for i, query := range queries {
output, err := ec2Client.DescribeInstances(ctx, query.Input)
if err != nil {
return nil, errors.BadRequestStatus("error running DescribeInstances for host set id %q: %s", query.Id, err)
return nil, errors.BadRequestStatusf("error running DescribeInstances for host set id %q: %s", query.Id, err)
}
queries[i].Output = output
@@ -550,9 +550,9 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
// set of hosts afterwards (possibly removing duplicates).
for _, reservation := range output.Reservations {
for _, instance := range reservation.Instances {
host, err := awsInstanceToHost(instance, catalogAttributes)
host, err := awsInstanceToHost(instance)
if err != nil {
return nil, errors.BadRequestStatus("error processing host results for host set id %q: %s", query.Id, err)
return nil, errors.BadRequestStatusf("error processing host results for host set id %q: %s", query.Id, err)
}
queries[i].OutputHosts = append(queries[i].OutputHosts, host)
@@ -648,13 +648,10 @@ func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.Descri
// awsInstanceToHost processes data from an ec2.Instance and returns
// a ListHostsResponseHost with the ID and network addresses
// populated.
func awsInstanceToHost(instance types.Instance, catalogAttributes *CatalogAttributes) (*pb.ListHostsResponseHost, error) {
func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, error) {
if aws.ToString(instance.InstanceId) == "" {
return nil, fmt.Errorf("response integrity error: missing instance id")
}
if catalogAttributes == nil {
return nil, fmt.Errorf("catalog attributes are required")
}
result := new(pb.ListHostsResponseHost)
@@ -669,46 +666,22 @@ func awsInstanceToHost(instance types.Instance, catalogAttributes *CatalogAttrib
}
}
if !catalogAttributes.ExcludePrivateIps {
result.IpAddresses = appendDistinct(result.IpAddresses, instance.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, instance.PrivateDnsName)
}
if !catalogAttributes.ExcludePublicIps {
result.IpAddresses = appendDistinct(result.IpAddresses, instance.PublicIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, instance.PublicDnsName)
}
result.IpAddresses = appendDistinct(result.IpAddresses, instance.PrivateIpAddress, instance.PublicIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, instance.PrivateDnsName, instance.PublicDnsName)
// Now go through all of the interfaces and log the IP address of
// every interface.
for _, iface := range instance.NetworkInterfaces {
if catalogAttributes.PrimaryInterfaceOnly {
if iface.Attachment == nil || aws.ToInt32(iface.Attachment.DeviceIndex) != 0 {
// We only want to sync the primary interface so skip any others
continue
}
}
// Populate default IP addresses/DNS name similar to how we do
// for the entire instance.
if !catalogAttributes.ExcludePrivateIps {
result.IpAddresses = appendDistinct(result.IpAddresses, iface.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, iface.PrivateDnsName)
}
result.IpAddresses = appendDistinct(result.IpAddresses, iface.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, iface.PrivateDnsName)
// Iterate through the private IP addresses and log the
// information.
for _, addr := range iface.PrivateIpAddresses {
if !catalogAttributes.ExcludePrivateIps {
result.IpAddresses = appendDistinct(result.IpAddresses, addr.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, addr.PrivateDnsName)
}
if catalogAttributes.ExcludePublicIps {
// skip adding public addresses if we are excluding them
continue
}
// PublicIP for iface is only available via an association to the private IP
result.IpAddresses = appendDistinct(result.IpAddresses, addr.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, addr.PrivateDnsName)
if addr.Association != nil {
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Association.PublicIp)
result.DnsNames = appendDistinct(result.DnsNames, addr.Association.PublicDnsName)
@@ -716,10 +689,8 @@ func awsInstanceToHost(instance types.Instance, catalogAttributes *CatalogAttrib
}
// Add the IPv6 addresses.
if !catalogAttributes.ExcludeIpv6 {
for _, addr := range iface.Ipv6Addresses {
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Ipv6Address)
}
for _, addr := range iface.Ipv6Addresses {
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Ipv6Address)
}
}
+11 -692
View File
@@ -638,8 +638,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
}},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
@@ -665,8 +664,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
}},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
@@ -696,8 +694,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
}},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
@@ -724,8 +721,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
}},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
@@ -2387,11 +2383,10 @@ func TestBuildDescribeInstancesInput(t *testing.T) {
func TestAwsInstanceToHost(t *testing.T) {
cases := []struct {
name string
instance types.Instance
catalogAttributes *CatalogAttributes
expected *pb.ListHostsResponseHost
expectedErr string
name string
instance types.Instance
expected *pb.ListHostsResponseHost
expectedErr string
}{
{
name: "missing instance id",
@@ -2596,45 +2591,14 @@ func TestAwsInstanceToHost(t *testing.T) {
},
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: nil}, // Just coverage for nil assertion which is skipped
{Ipv6Address: aws.String("2001:db8::1")},
{Ipv6Address: aws.String("some::fake::address")},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "2001:db8::1"},
DnsNames: []string{"test.example.internal", "test.example.com"},
},
},
{
name: "good, single IP w/public addr",
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1"},
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "some::fake::address"},
DnsNames: []string{"test.example.internal", "test.example.com"},
},
},
@@ -2712,663 +2676,18 @@ func TestAwsInstanceToHost(t *testing.T) {
DnsNames: []string{"test.example.internal", "test.example.com"},
},
},
{
name: "primary_interface_only keeps all addresses on primary ENI",
catalogAttributes: &CatalogAttributes{
PrimaryInterfaceOnly: true,
},
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(0),
},
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Primary: aws.Bool(true),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
},
// Secondary IPs on primary ENI (e.g. VPC CNI pod IPs).
{
Primary: aws.Bool(false),
PrivateIpAddress: aws.String("10.0.0.42"),
PrivateDnsName: aws.String("pod-a.example.internal"),
},
{
Primary: aws.Bool(false),
PrivateIpAddress: aws.String("10.0.0.43"),
PrivateDnsName: aws.String("pod-b.example.internal"),
},
},
},
// Secondary ENI attached by VPC CNI for additional pod IPs.
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(1),
},
PrivateIpAddress: aws.String("10.0.0.99"),
PrivateDnsName: aws.String("eni1.example.internal"),
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "10.0.0.42", "10.0.0.43"},
DnsNames: []string{"test.example.internal", "test.example.com", "pod-a.example.internal", "pod-b.example.internal"},
},
},
{
name: "primary_interface_only keeps IPv6 on primary ENI",
catalogAttributes: &CatalogAttributes{
PrimaryInterfaceOnly: true,
},
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
NetworkInterfaces: []types.InstanceNetworkInterface{
// Secondary ENI listed first to verify ordering does not matter.
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(1),
},
PrivateIpAddress: aws.String("10.0.0.99"),
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: aws.String("2001:db8::2")},
},
},
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(0),
},
PrivateIpAddress: aws.String("10.0.0.1"),
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: aws.String("2001:db8::3")},
{Ipv6Address: aws.String("2001:db8::4")},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "2001:db8::3", "2001:db8::4"},
DnsNames: []string{"test.example.internal"},
},
},
{
name: "primary_interface_only without interfaces still returns top-level IPs",
catalogAttributes: &CatalogAttributes{
PrimaryInterfaceOnly: true,
},
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1"},
DnsNames: []string{"test.example.internal", "test.example.com"},
},
},
{
name: "exclude_public_ips excludes public IPv4 sources but keeps IPv6",
catalogAttributes: &CatalogAttributes{
ExcludePublicIps: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::1")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "2001:db8::1"},
DnsNames: []string{"host.internal"},
},
},
{
name: "exclude_private_ips excludes private IPv4 sources but keeps IPv6",
catalogAttributes: &CatalogAttributes{
ExcludePrivateIps: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{Association: &types.InstanceNetworkInterfaceAssociation{PublicIp: aws.String("1.2.3.4"), PublicDnsName: aws.String("host.public")}},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::1")}},
},
{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{Association: &types.InstanceNetworkInterfaceAssociation{PublicIp: aws.String("1.2.3.5"), PublicDnsName: aws.String("svc.public")}},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"1.2.3.4", "2001:db8::1", "1.2.3.5"},
DnsNames: []string{"host.public", "svc.public"},
},
},
{
name: "exclude_public_ips keeps IPv6 addresses",
catalogAttributes: &CatalogAttributes{
ExcludePublicIps: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("fd00::1")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "fd00::1"},
DnsNames: []string{"host.internal"},
},
},
{
name: "exclude_ipv6 omits IPv6 while preserving IPv4 and DNS",
catalogAttributes: &CatalogAttributes{
ExcludeIpv6: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::1")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "1.2.3.4"},
DnsNames: []string{"host.internal", "host.public"},
},
},
{
name: "exclude_public_ips with exclude_ipv6 keeps only private IPv4 sources",
catalogAttributes: &CatalogAttributes{
ExcludePublicIps: true,
ExcludeIpv6: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
{
PrivateIpAddress: aws.String("10.0.0.2"),
PrivateDnsName: aws.String("svc.internal"),
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::1")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "10.0.0.2"},
DnsNames: []string{"host.internal", "svc.internal"},
},
},
{
name: "exclude_private_ips with exclude_ipv6 keeps only public IPv4 sources",
catalogAttributes: &CatalogAttributes{
ExcludePrivateIps: true,
ExcludeIpv6: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.5"),
PublicDnsName: aws.String("svc.public"),
},
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("fd00::1")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"1.2.3.4", "1.2.3.5"},
DnsNames: []string{"host.public", "svc.public"},
},
},
{
name: "primary_interface_only with exclude_ipv6 drops IPv6 from primary ENI",
catalogAttributes: &CatalogAttributes{
PrimaryInterfaceOnly: true,
ExcludeIpv6: true,
},
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(0),
},
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.42"),
PrivateDnsName: aws.String("pod-a.example.internal"),
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::3")}},
},
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(1),
},
PrivateIpAddress: aws.String("10.0.0.99"),
PrivateDnsName: aws.String("eni1.example.internal"),
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::4")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "10.0.0.42"},
DnsNames: []string{"test.example.internal", "test.example.com", "pod-a.example.internal"},
},
},
{
name: "exclude_ipv6 on IPv6-only interface leaves no addresses",
catalogAttributes: &CatalogAttributes{
ExcludeIpv6: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: aws.String("2001:db8::1")},
{Ipv6Address: aws.String("fd00::1")},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
},
},
{
name: "deduplicates repeated addresses and DNS names across instance and interfaces",
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
},
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: aws.String("2001:db8::1")},
{Ipv6Address: aws.String("2001:db8::1")},
},
},
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
},
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: aws.String("2001:db8::1")},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "1.2.3.4", "2001:db8::1"},
DnsNames: []string{"host.internal", "host.public"},
},
},
{
name: "primary_interface_only skips interfaces without attachment metadata",
catalogAttributes: &CatalogAttributes{
PrimaryInterfaceOnly: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.99"),
PrivateDnsName: aws.String("missing-attachment.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.99"),
PrivateDnsName: aws.String("missing-attachment.internal"),
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::99")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "1.2.3.4"},
DnsNames: []string{"host.internal", "host.public"},
},
},
{
name: "exclude_private_ips removes private DNS names even when public addresses remain",
catalogAttributes: &CatalogAttributes{
ExcludePrivateIps: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
{
PrivateIpAddress: aws.String("10.0.0.2"),
PrivateDnsName: aws.String("svc.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.5"),
PublicDnsName: aws.String("svc.public"),
},
},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"1.2.3.4", "1.2.3.5"},
DnsNames: []string{"host.public", "svc.public"},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
attrs := tc.catalogAttributes
if attrs == nil {
attrs = &CatalogAttributes{}
}
actual, err := awsInstanceToHost(tc.instance, attrs)
actual, err := awsInstanceToHost(tc.instance)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
func TestGetCatalogAttributesAddressSelection(t *testing.T) {
cases := []struct {
name string
in *structpb.Struct
expectedErr string
expected *CatalogAttributes
}{
{
name: "valid selectors",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"primary_interface_only": structpb.NewBoolValue(true),
"exclude_public_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
PrimaryInterfaceOnly: true,
ExcludePublicIps: true,
ExcludeIpv6: true,
},
},
{
name: "exclude_private_ips can be combined with exclude_ipv6",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePrivateIps: true,
ExcludeIpv6: true,
},
},
{
name: "exclude_public_ips can be combined with exclude_ipv6",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_public_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePublicIps: true,
ExcludeIpv6: true,
},
},
{
name: "exclude_private_ips and exclude_public_ips can be combined when IPv6 remains enabled",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_public_ips": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePrivateIps: true,
ExcludePublicIps: true,
},
},
{
name: "exclude_private_ips and exclude_public_ips cannot also exclude_ipv6",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_public_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expectedErr: "attributes.exclude_ipv6: cannot be combined with exclude_private_ips and exclude_public_ips, attributes.exclude_private_ips: cannot be combined with exclude_public_ips and exclude_ipv6, attributes.exclude_public_ips: cannot be combined with exclude_private_ips and exclude_ipv6",
},
{
name: "exclude_ipv6 alone is accepted",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludeIpv6: true,
},
},
{
name: "exclude_ipv6 requires a boolean",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_ipv6": structpb.NewStringValue("true"),
},
},
expectedErr: "attributes.exclude_ipv6",
},
{
name: "no selectors configured preserves historical behavior",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := getCatalogAttributes(tc.in)
if tc.expectedErr != "" {
require.Error(err)
require.Contains(err.Error(), tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
+4
View File
@@ -40,6 +40,10 @@ func setStatePermission(req any, permission *pb.Permission) *pb.StorageBucketCre
state.State.Delete = permission
case *pb.DeleteObjectsRequest:
state.State.Delete = permission
case pb.ListObjectsRequest:
state.State.Read = permission
case *pb.ListObjectsRequest:
state.State.Read = permission
}
return state
}
+215 -29
View File
@@ -9,6 +9,7 @@ import (
"context"
"crypto/sha256"
"encoding/base64"
stderr "errors"
"fmt"
"io"
"os"
@@ -76,7 +77,7 @@ func (p *StoragePlugin) getClient(ctx context.Context,
// No storage bucket ID to key cache on, create new client and return
client, err := storageState.s3Client(ctx, opt...)
if err != nil {
return nil, errors.BadRequestStatus("error creating S3 client: %s", err)
return nil, errors.BadRequestStatusf("error creating S3 client: %s", err)
}
return client, nil
}
@@ -92,7 +93,7 @@ func (p *StoragePlugin) getClient(ctx context.Context,
defer p.clients.Unlock()
client, err := storageState.s3Client(ctx, opt...)
if err != nil {
return nil, errors.BadRequestStatus("error creating S3 client: %s", err)
return nil, errors.BadRequestStatusf("error creating S3 client: %s", err)
}
p.clients.cache[storageBucketId] = client
return client, nil
@@ -118,7 +119,7 @@ func (p *StoragePlugin) getClient(ctx context.Context,
var err error
client, err = storageState.s3Client(ctx, opt...)
if err != nil {
return nil, errors.BadRequestStatus("error creating S3 client: %s", err)
return nil, errors.BadRequestStatusf("error creating S3 client: %s", err)
}
p.clients.cache[storageBucketId] = client
}
@@ -138,7 +139,7 @@ func (p *StoragePlugin) call(
s3Client, err := p.getClient(ctx, storageBucketId, storageState, opts...)
if err != nil {
return nil, errors.BadRequestStatus("error getting S3 client: %s", err)
return nil, errors.BadRequestStatusf("error getting S3 client: %s", err)
}
firstAttempt := true
for {
@@ -151,7 +152,7 @@ func (p *StoragePlugin) call(
opts = append(opts, WithCacheRefresh(true))
s3Client, err = p.getClient(ctx, storageBucketId, storageState, opts...)
if err != nil {
return nil, errors.BadRequestStatus("error refreshing S3 client: %s", err)
return nil, errors.BadRequestStatusf("error refreshing S3 client: %s", err)
}
continue
}
@@ -218,7 +219,7 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
// Try to rotate AWS static credentials
@@ -234,7 +235,7 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
}, p.testStorageStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
// perform dry run to ensure we can interact with the bucket as expected.
@@ -244,7 +245,7 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
persistedProto, err := storageState.toProto()
if err != nil {
return nil, errors.BadRequestStatus("%s", err.Error())
return nil, errors.BadRequestStatus(err.Error())
}
return &pb.OnCreateStorageBucketResponse{
@@ -292,7 +293,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
oldStorageAttributes.DualStack,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
// Verify the incoming credentials are valid and return any errors to the
@@ -315,7 +316,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up new credential persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up new credential persisted state: %s", err)
}
newStorageState, err := newAwsStoragePersistedState(
append([]awsStoragePersistedStateOption{
@@ -323,7 +324,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
}, p.testStorageStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
if st := dryRunValidation(ctx, newStorageState, newStorageAttributes, newBucket); st != nil {
return nil, st.Err()
@@ -363,7 +364,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
}, p.testStorageStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
// perform dry run to ensure we can interact with the bucket as expected.
@@ -373,7 +374,7 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
persistedProto, err := storageState.toProto()
if err != nil {
return nil, errors.BadRequestStatus("%s", err.Error())
return nil, errors.BadRequestStatus(err.Error())
}
return &pb.OnUpdateStorageBucketResponse{
@@ -410,7 +411,7 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
storageAttributes.DualStack,
p.testCredStateOpts...)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
_, err = newAwsStoragePersistedState(
@@ -419,7 +420,7 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
}, p.testStorageStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error loading persisted state: %s", err)
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
}
// try to delete static credentials
@@ -474,7 +475,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
storageState, err := newAwsStoragePersistedState(
@@ -483,7 +484,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
}, p.testStorageStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
opts := []s3Option{}
@@ -546,7 +547,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
storageState, err := newAwsStoragePersistedState(
@@ -555,7 +556,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
}, p.testStorageStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
// perform dry run to ensure we can interact with the bucket as expected.
@@ -604,7 +605,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
}, p.testCredStateOpts...)...,
)
if err != nil {
return errors.BadRequestStatus("error setting up persisted state: %s", err)
return errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
storageState, err := newAwsStoragePersistedState(
@@ -613,7 +614,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
}, p.testStorageStateOpts...)...,
)
if err != nil {
return errors.BadRequestStatus("error setting up persisted state: %s", err)
return errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
opts := []s3Option{}
@@ -647,13 +648,13 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
buffer := make([]byte, chunkSize)
n, err := reader.Read(buffer)
if err != nil && err != io.EOF {
return errors.UnknownStatus("error reading chunk from s3: %s", err)
return errors.UnknownStatusf("error reading chunk from s3: %s", err)
}
if n > 0 {
if err := stream.Send(&pb.GetObjectResponse{
FileChunk: buffer[:n],
}); err != nil {
return errors.UnknownStatus("error sending chunk to client: %s", err)
return errors.UnknownStatusf("error sending chunk to client: %s", err)
}
}
if err == io.EOF {
@@ -718,7 +719,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
storageState, err := newAwsStoragePersistedState(
@@ -727,7 +728,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
}, p.testStorageStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
opts := []s3Option{}
@@ -817,7 +818,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
storageState, err := newAwsStoragePersistedState(
@@ -826,7 +827,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
}, p.testStorageStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatus("error setting up persisted state: %s", err)
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
opts := []s3Option{}
@@ -861,7 +862,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
objects := []types.ObjectIdentifier{}
client, err := p.getClient(ctx, bucket.GetId(), storageState, opts...)
if err != nil {
return nil, errors.BadRequestStatus("error getting S3 client: %s", err)
return nil, errors.BadRequestStatusf("error getting S3 client: %s", err)
}
var conToken *string
truncated, firstAttempt := true, true
@@ -880,7 +881,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
opts = append(opts, WithCacheRefresh(true))
client, err = p.getClient(ctx, bucket.GetId(), storageState, opts...)
if err != nil {
return nil, errors.BadRequestStatus("error refreshing S3 client: %s", err)
return nil, errors.BadRequestStatusf("error refreshing S3 client: %s", err)
}
continue
}
@@ -917,6 +918,191 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
}, nil
}
// ListObjects retrieves files and directories from an S3 bucket under a given prefix.
// If the "recursive" flag in the request is false (default), it returns only the top-level objects directly under the prefix.
// If the "recursive" is true, it returns all objects under the prefix recursively.
func (p *StoragePlugin) ListObjects(ctx context.Context, req *pb.ListObjectsRequest) (*pb.ListObjectsResponse, error) {
bucket := req.GetBucket()
if bucket == nil {
return nil, errors.BadRequestStatus("bucket is required")
}
if bucket.BucketName == "" {
return nil, errors.BadRequestStatus("bucketName is required")
}
attrs := bucket.GetAttributes()
if attrs == nil {
return nil, errors.BadRequestStatus("attributes is required")
}
storageAttributes, err := getStorageAttributes(attrs)
if err != nil {
return nil, err
}
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
if err != nil {
return nil, err
}
// Set up the creds in our persisted state.
credState, err := cred.NewAwsCredentialPersistedState(
append([]cred.AwsCredentialPersistedStateOption{
cred.WithCredentialsConfig(credConfig),
}, p.testCredStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
storageState, err := newAwsStoragePersistedState(
append([]awsStoragePersistedStateOption{
withCredentials(credState),
}, p.testStorageStateOpts...)...,
)
if err != nil {
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
}
var opts []s3Option
if storageAttributes.EndpointUrl != "" {
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
}
s3Client, err := storageState.s3Client(ctx, opts...)
if err != nil {
return nil, errors.BadRequestStatusf("error getting S3 client: %s", err)
}
prefix := path.Join(bucket.GetBucketPrefix(), req.GetKeyPrefix())
if strings.HasSuffix(req.GetKeyPrefix(), "/") {
// path.Join ends by "cleaning" the path, including removing a trailing slash, if
// it exists. given that a slash is used to denote a folder, it is required here.
prefix += "/"
}
var objects []*pb.Object
if req.GetRecursive() {
objects, err = listObjectsRecursive(ctx, s3Client, bucket.GetBucketName(), prefix, req)
} else {
objects, err = listObjectsTopLevel(ctx, s3Client, bucket.GetBucketName(), prefix, req)
}
if err != nil {
return nil, err
}
return &pb.ListObjectsResponse{
Objects: objects,
}, nil
}
// listObjectsTopLevel lists only the objects that exist directly under the specified prefix (This does not traverse subdirectories)
// It uses the S3 "Delimiter" option to group results so that subdirectories appear as CommonPrefixes.
func listObjectsTopLevel(ctx context.Context, s3Client S3API, bucketName, prefix string, req *pb.ListObjectsRequest) ([]*pb.Object, error) {
var objects []*pb.Object
paginator := s3.NewListObjectsV2Paginator(s3Client, &s3.ListObjectsV2Input{
Bucket: aws.String(bucketName),
Prefix: aws.String(prefix),
Delimiter: aws.String("/"),
})
for paginator.HasMorePages() {
if err := ctx.Err(); err != nil {
switch {
case stderr.Is(err, context.Canceled):
return nil, status.Error(codes.Canceled, "request canceled")
case stderr.Is(err, context.DeadlineExceeded):
return nil, status.Error(codes.DeadlineExceeded, "request deadline exceeded")
default:
return nil, status.Error(codes.Internal, "unknown context error")
}
}
page, err := paginator.NextPage(ctx)
if err != nil {
return nil, parseS3Error("failed to list objects", err, req).Err()
}
for _, c := range page.Contents {
if c.Key != nil {
objects = append(objects, &pb.Object{Key: *c.Key, IsDir: false})
}
}
for _, cp := range page.CommonPrefixes {
if cp.Prefix != nil {
objects = append(objects, &pb.Object{Key: *cp.Prefix, IsDir: true})
}
}
}
return objects, nil
}
// listObjectsRecursive lists all objects under the specified prefix, traversing all subdirectories.
// It does not use the S3 "Delimiter" option, so every object key that matches the prefix is returned.
// This mode is useful for fully scanning all objects in a folder tree.
func listObjectsRecursive(ctx context.Context, s3Client S3API, bucketName, prefix string, req *pb.ListObjectsRequest) ([]*pb.Object, error) {
var objects []*pb.Object
folderSet := make(map[string]struct{})
paginator := s3.NewListObjectsV2Paginator(s3Client, &s3.ListObjectsV2Input{
Bucket: aws.String(bucketName),
Prefix: aws.String(prefix),
})
for paginator.HasMorePages() {
if err := ctx.Err(); err != nil {
switch {
case stderr.Is(err, context.Canceled):
return nil, status.Error(codes.Canceled, "request canceled")
case stderr.Is(err, context.DeadlineExceeded):
return nil, status.Error(codes.DeadlineExceeded, "request deadline exceeded")
default:
return nil, status.Error(codes.Internal, "unknown context error")
}
}
page, err := paginator.NextPage(ctx)
if err != nil {
return nil, parseS3Error("failed to list objects", err, req).Err()
}
for _, c := range page.Contents {
// We intentionally ignore "empty objects" that simulate folders.
// S3 is not a traditional file system and does not have real directories.
// Folders are an abstraction; they are represented by zero-byte objects
// whose keys end with a slash (e.g., "myfolder/"). These objects exist
// solely as placeholders to give the appearance of directories in S3 tools.
// Here, we only include actual objects with content (non-zero length).
if c.Key != nil && !strings.HasSuffix(*c.Key, "/") {
objects = append(objects, &pb.Object{Key: *c.Key, IsDir: false})
}
// Extract all parent folder paths from the object key.
// We walk up the directory hierarchy using path.Dir,
// adding each parent folder to folderSet until we reach
// the root ("/" or ".") or leave the current prefix subtree.
// This ensures all intermediate "folders" appear in the result even if
// no explicit zero-byte object exists for them.
dir := path.Dir(*c.Key)
for dir != "." && dir != "/" {
folder := dir + "/"
if !strings.HasPrefix(folder, prefix) {
break // left the subtree, stop climbing up
}
if folder != prefix {
folderSet[folder] = struct{}{}
}
dir = path.Dir(dir)
}
}
}
// Add folders
for folder := range folderSet {
objects = append(objects, &pb.Object{
Key: folder,
IsDir: true,
})
}
return objects, nil
}
// dryRunValidation validates that the IAM role policy attached to the given secrets of the
// storage bucket has the minimum required permissions needed for this plugin to function
// as expected. This function will create an object in the s3 bucket to validate it has
+388
View File
@@ -4202,3 +4202,391 @@ func TestS3ClientCache(t *testing.T) {
assert.Equal(cred3.AccessKey, client.Credentials().AccessKeyID)
assert.Equal(cred3.SecretKey, client.Credentials().SecretAccessKey)
}
func TestStoragePlugin_ListObjects(t *testing.T) {
validRequest := func() *pb.ListObjectsRequest {
return &pb.ListObjectsRequest{
KeyPrefix: "foo/bar/",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
}
}
cases := []struct {
name string
req *pb.ListObjectsRequest
credOpts []credential.AwsCredentialPersistedStateOption
storageOpts []awsStoragePersistedStateOption
ctx context.Context
expectedObjects []*pb.Object
expectedErrContains string
expectedErrCode codes.Code
expectedDetails *pb.StorageBucketCredentialState
}{
{
name: "nil bucket",
req: &pb.ListObjectsRequest{KeyPrefix: "foo/bar"},
expectedErrContains: "bucket is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "empty bucket name",
req: &pb.ListObjectsRequest{
KeyPrefix: "foo/bar",
Bucket: &storagebuckets.StorageBucket{},
},
expectedErrContains: "bucketName is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes",
req: &pb.ListObjectsRequest{
KeyPrefix: "foo/bar",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
},
},
expectedErrContains: "attributes is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading attributes",
req: &pb.ListObjectsRequest{
KeyPrefix: "foo/bar",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: new(structpb.Struct),
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "credential persisted state setup error",
req: validRequest(),
credOpts: []credential.AwsCredentialPersistedStateOption{
func(s *credential.AwsCredentialPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "storage persisted state setup error",
req: validRequest(),
storageOpts: []awsStoragePersistedStateOption{
func(s *awsStoragePersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "listObjectsV2 access denied error",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithListObjectsV2Error(TestAwsS3Error("AccessDenied", "ListObjectsV2", "not authorized")),
),
),
},
expectedErrContains: "aws service s3: invalid credentials: failed to list objects",
expectedErrCode: codes.PermissionDenied,
expectedDetails: &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Read: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "not authorized",
CheckedAt: timestamppb.Now(),
},
},
},
},
{
name: "listObjectsV2 throttling error",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithListObjectsV2Error(TestAwsS3Error("ThrottlingException", "ListObjectsV2", "throttling exception")),
),
),
},
expectedErrContains: "aws service s3: throttling: failed to list objects",
expectedErrCode: codes.Unavailable,
expectedDetails: &pb.StorageBucketCredentialState{
State: &pb.Permissions{
Read: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
CheckedAt: timestamppb.Now(),
},
},
},
},
{
name: "context canceled - non recursive",
req: validRequest(),
ctx: func() context.Context {
c, cancel := context.WithCancel(context.Background())
cancel()
return c
}(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
// Return an error that simulates context cancellation being detected at the AWS level
testMockS3WithListObjectsV2Error(context.Canceled),
),
),
},
expectedErrContains: "request canceled",
expectedErrCode: codes.Canceled,
},
{
name: "context deadline exceeded - non recursive",
req: validRequest(),
ctx: func() context.Context {
c, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
time.Sleep(2 * time.Nanosecond) // Ensure deadline is exceeded
cancel()
return c
}(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
// Return an error that simulates deadline exceeded being detected at the AWS level
testMockS3WithListObjectsV2Error(context.DeadlineExceeded),
),
),
},
expectedErrContains: "request deadline exceeded",
expectedErrCode: codes.DeadlineExceeded,
},
{
name: "context canceled - recursive",
req: func() *pb.ListObjectsRequest {
r := validRequest()
r.Recursive = true
return r
}(),
ctx: func() context.Context {
c, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
return c
}(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
// Return an error that simulates context cancellation being detected at the AWS level
testMockS3WithListObjectsV2Error(context.Canceled),
),
),
},
expectedErrContains: "request canceled",
expectedErrCode: codes.Canceled,
},
{
name: "context deadline exceeded - recursive",
req: func() *pb.ListObjectsRequest {
r := validRequest()
r.Recursive = true
return r
}(),
ctx: func() context.Context {
c, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
// Ensure the deadline is exceeded
time.Sleep(2 * time.Nanosecond)
cancel()
return c
}(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
// Return an error that simulates deadline exceeded being detected at the AWS level
testMockS3WithListObjectsV2Error(context.DeadlineExceeded),
),
),
},
expectedErrContains: "request deadline exceeded",
expectedErrCode: codes.DeadlineExceeded,
},
{
name: "success - single page top-level",
req: validRequest(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithListObjectsV2Output(&s3.ListObjectsV2Output{
CommonPrefixes: []s3types.CommonPrefix{
{Prefix: aws.String("foo/bar/key/")},
},
Contents: []s3types.Object{
{Key: aws.String("foo/bar/file1.bsr")},
{Key: aws.String("foo/bar/file2.bsr")},
},
}),
),
),
},
expectedObjects: []*pb.Object{
{Key: "foo/bar/key/", IsDir: true},
{Key: "foo/bar/file1.bsr", IsDir: false},
{Key: "foo/bar/file2.bsr", IsDir: false},
},
},
{
name: "success - recursive listing with nested dirs",
req: func() *pb.ListObjectsRequest {
r := validRequest()
r.Recursive = true
return r
}(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithListObjectsV2Output(&s3.ListObjectsV2Output{
Contents: []s3types.Object{
{Key: aws.String("foo/bar/file1.bsr")},
{Key: aws.String("foo/bar/nested/file2.bsr")},
{Key: aws.String("foo/bar/nested/deeper/file3.bsr")},
},
}),
),
),
},
expectedObjects: []*pb.Object{
{Key: "foo/bar/file1.bsr", IsDir: false},
{Key: "foo/bar/nested/", IsDir: true},
{Key: "foo/bar/nested/file2.bsr", IsDir: false},
{Key: "foo/bar/nested/deeper/", IsDir: true},
{Key: "foo/bar/nested/deeper/file3.bsr", IsDir: false},
},
},
{
name: "success - recursive multi-page",
req: func() *pb.ListObjectsRequest {
r := validRequest()
r.Recursive = true
return r
}(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(
nil,
testMockS3WithListObjectsV2OutputFunc(func(input *s3.ListObjectsV2Input) *s3.ListObjectsV2Output {
if input.ContinuationToken == nil {
return &s3.ListObjectsV2Output{
Contents: []s3types.Object{
{Key: aws.String("foo/bar/page1/file1.bsr")},
},
NextContinuationToken: aws.String("token123"),
IsTruncated: aws.Bool(true),
}
}
if aws.ToString(input.ContinuationToken) == "token123" {
return &s3.ListObjectsV2Output{
Contents: []s3types.Object{
{Key: aws.String("foo/bar/page2/file2.bsr")},
},
}
}
return &s3.ListObjectsV2Output{}
}),
),
),
},
expectedObjects: []*pb.Object{
{Key: "foo/bar/page1/", IsDir: true},
{Key: "foo/bar/page1/file1.bsr", IsDir: false},
{Key: "foo/bar/page2/", IsDir: true},
{Key: "foo/bar/page2/file2.bsr", IsDir: false},
},
},
{
name: "success - recursive empty",
req: func() *pb.ListObjectsRequest {
r := validRequest()
r.Recursive = true
return r
}(),
credOpts: validSTSMock(),
storageOpts: []awsStoragePersistedStateOption{
withTestS3APIFunc(
newTestMockS3(nil, testMockS3WithListObjectsV2Output(&s3.ListObjectsV2Output{})),
),
},
expectedObjects: []*pb.Object(nil),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
p := &StoragePlugin{
testCredStateOpts: tc.credOpts,
testStorageStateOpts: tc.storageOpts,
}
ctx := tc.ctx
if ctx == nil {
ctx = context.Background()
}
resp, err := p.ListObjects(ctx, tc.req)
if tc.expectedErrContains != "" {
require.Error(err)
assert.Contains(err.Error(), tc.expectedErrContains)
assert.Equal(tc.expectedErrCode, status.Code(err))
st, ok := status.FromError(err)
require.True(ok)
if tc.expectedDetails != nil {
var state *pb.StorageBucketCredentialState
for _, detail := range st.Details() {
if errDetail, ok := detail.(*pb.StorageBucketCredentialState); ok {
state = errDetail
break
}
}
require.NotNil(state)
internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Read, state.State.Read, false)
internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Write, state.State.Write, false)
internal.CheckSimilarPermission(assert, tc.expectedDetails.State.Delete, state.State.Delete, false)
} else {
assert.Len(st.Details(), 0)
}
return
}
require.NoError(err)
require.NotNil(resp)
require.ElementsMatch(resp.Objects, tc.expectedObjects)
})
}
}
+15 -9
View File
@@ -22,8 +22,6 @@ import (
const expectedEc2InstanceCount = 5
var expectedTags = []string{"foo", "bar", "baz"}
func TestHostPlugin(t *testing.T) {
region := os.Getenv("AWS_REGION")
if region == "" {
@@ -80,6 +78,14 @@ func TestHostPlugin(t *testing.T) {
require.NoError(err)
require.Len(ec2InstanceTags, expectedEc2InstanceCount)
rawExpectedTags, err := tf.GetOutputSlice("instance_tag_keys")
require.NoError(err)
require.Len(rawExpectedTags, 3)
expectedTags := make([]string, 0, len(rawExpectedTags))
for _, rawExpectedTag := range rawExpectedTags {
expectedTags = append(expectedTags, rawExpectedTag.(string))
}
// Start the workflow now. Set up the host catalog. Note that this
// will cause the state to go out of drift above in the sense that
// the access key ID/secret access key will no longer be valid. We
@@ -143,13 +149,13 @@ func TestHostPlugin(t *testing.T) {
}
cases := [][]string{
{"foo"},
{"bar"},
{"baz"},
{"foo", "bar"},
{"foo", "baz"},
{"bar", "baz"},
{"foo", "bar", "baz"},
{expectedTags[0]},
{expectedTags[1]},
{expectedTags[2]},
{expectedTags[0], expectedTags[1]},
{expectedTags[0], expectedTags[2]},
{expectedTags[1], expectedTags[2]},
{expectedTags[0], expectedTags[1], expectedTags[2]},
}
for _, tc := range cases {
+207
View File
@@ -5,6 +5,7 @@ package testing
import (
"context"
"fmt"
"io"
"net"
"os"
@@ -85,6 +86,12 @@ func TestStoragePlugin(t *testing.T) {
iamSecretAccessKeyMissingDeleteObject, err := tf.GetOutputString("iam_secret_access_key_missing_delete_obj")
require.NoError(err)
s3PaginationTestObjectCount, err := tf.GetOutput("s3_pagination_test_object_count")
require.NoError(err)
objectCountFloat, ok := s3PaginationTestObjectCount.(float64)
require.True(ok)
objectCount := int(objectCountFloat)
// Start the workflow now. Set up the storage bucket. Note that this
// will cause the state to go out of drift above in the sense that
// the access key ID/secret access key will no longer be valid. We
@@ -138,6 +145,11 @@ func TestStoragePlugin(t *testing.T) {
keyid, secret = iamAccessKeyIds[5].(string), iamSecretAccessKeys[5].(string)
testPluginObjectMethods(ctx, t, p, bucketName, region, keyid, secret)
// ********************
// * ListObjects
// ********************
testListObjects(ctx, t, p, objectCount, bucketName, region, keyid, secret)
// ********************
// * Validate Permissions
// ********************
@@ -514,3 +526,198 @@ func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.Stora
require.NoError(conn.Close())
grpcServer.Stop()
}
// testListObjects verifies top-level and recursive listing by matching the exact keys and types of returned objects.
func testListObjects(ctx context.Context, t *testing.T, p *storage.StoragePlugin, objectCount int, bucketName, region, accessKeyId, secretAccessKey string) {
t.Helper()
t.Logf("testing ListObjects (bucket=%s, region=%s, expectedObjects=%d, creds=%t)",
bucketName,
region,
objectCount,
accessKeyId != "" && secretAccessKey != "",
)
r := require.New(t)
reqAttrs, err := structpb.NewStruct(map[string]any{
credential.ConstRegion: region,
credential.ConstDisableCredentialRotation: true,
})
r.NoError(err)
reqSecrets, err := structpb.NewStruct(map[string]any{
credential.ConstAccessKeyId: accessKeyId,
credential.ConstSecretAccessKey: secretAccessKey,
})
r.NoError(err)
bucket := &storagebuckets.StorageBucket{
BucketName: bucketName,
Attributes: reqAttrs,
Secrets: reqSecrets,
}
// Test Scenario 1: Non-Paginated Listing
t.Run("NonPaginatedListing", func(t *testing.T) {
// Subtest for the non-recursive (top-level) option
t.Run("TopLevel", func(t *testing.T) {
r := require.New(t)
t.Logf("testing ListObjects for a small set with recursive=false")
req := &pb.ListObjectsRequest{
Bucket: bucket,
KeyPrefix: "list-objects/",
Recursive: false,
}
resp, err := p.ListObjects(ctx, req)
r.NoError(err)
r.NotNil(resp)
expectedObjects := []*pb.Object{
{Key: "list-objects/file1.txt", IsDir: false},
{Key: "list-objects/data.json", IsDir: false},
{Key: "list-objects/nested-dir/", IsDir: true},
{Key: "list-objects/empty-nested-dir/", IsDir: true},
}
r.ElementsMatch(resp.Objects, expectedObjects)
})
// Subtest for the recursive option
t.Run("Recursive", func(t *testing.T) {
r := require.New(t)
t.Logf("testing ListObjects for a small set with recursive=true")
req := &pb.ListObjectsRequest{
Bucket: bucket,
KeyPrefix: "list-objects/",
Recursive: true,
}
resp, err := p.ListObjects(ctx, req)
r.NoError(err)
r.NotNil(resp)
expectedObjects := []*pb.Object{
{Key: "list-objects/file1.txt", IsDir: false},
{Key: "list-objects/data.json", IsDir: false},
{Key: "list-objects/nested-dir/file2.txt", IsDir: false},
{Key: "list-objects/nested-dir/", IsDir: true},
{Key: "list-objects/empty-nested-dir/", IsDir: true},
}
r.ElementsMatch(resp.Objects, expectedObjects)
})
})
// Test Scenario 2: Paginated Top-Level Listing
t.Run("TopLevelListing", func(t *testing.T) {
// Subtest for the root prefix
t.Run("AtRoot", func(t *testing.T) {
r := require.New(t)
t.Logf("testing ListObjects with recursive=false at root prefix")
req := &pb.ListObjectsRequest{
Bucket: bucket,
KeyPrefix: "list-objects-paginated/",
Recursive: false,
}
resp, err := p.ListObjects(ctx, req)
r.NoError(err)
r.NotNil(resp)
var expectedObjects []*pb.Object
for i := 0; i < objectCount; i++ {
key := fmt.Sprintf("list-objects-paginated/root-file-%d.txt", i)
expectedObjects = append(expectedObjects, &pb.Object{Key: key, IsDir: false})
}
for i := 0; i < objectCount; i++ {
keyEmpty := fmt.Sprintf("list-objects-paginated/empty-dir-%d/", i)
expectedObjects = append(expectedObjects, &pb.Object{Key: keyEmpty, IsDir: true})
keyNested := fmt.Sprintf("list-objects-paginated/nested-dir-%d/", i)
expectedObjects = append(expectedObjects, &pb.Object{Key: keyNested, IsDir: true})
}
r.ElementsMatch(resp.Objects, expectedObjects)
})
// Subtest for a nested prefix
t.Run("AtNestedPrefix", func(t *testing.T) {
r := require.New(t)
t.Logf("testing ListObjects with recursive=false at a nested prefix")
req := &pb.ListObjectsRequest{
Bucket: bucket,
KeyPrefix: "list-objects-paginated/nested-dir-7/",
Recursive: false,
}
resp, err := p.ListObjects(ctx, req)
r.NoError(err)
r.NotNil(resp)
expectedObjects := []*pb.Object{
{Key: "list-objects-paginated/nested-dir-7/data-file-7.json", IsDir: false},
}
r.ElementsMatch(resp.Objects, expectedObjects)
})
})
// Test Scenario 3: Paginated Recursive Listing
t.Run("RecursiveListing", func(t *testing.T) {
r := require.New(t)
t.Logf("testing ListObjects with recursive=true")
req := &pb.ListObjectsRequest{
Bucket: bucket,
KeyPrefix: "list-objects-paginated/",
Recursive: true,
}
resp, err := p.ListObjects(ctx, req)
r.NoError(err)
r.NotNil(resp)
var expectedObjects []*pb.Object
for i := 0; i < objectCount; i++ {
keyFile := fmt.Sprintf("list-objects-paginated/root-file-%d.txt", i)
expectedObjects = append(expectedObjects, &pb.Object{Key: keyFile, IsDir: false})
keyNestedFile := fmt.Sprintf("list-objects-paginated/nested-dir-%d/data-file-%d.json", i, i)
expectedObjects = append(expectedObjects, &pb.Object{Key: keyNestedFile, IsDir: false})
}
for i := 0; i < objectCount; i++ {
keyNestedDir := fmt.Sprintf("list-objects-paginated/nested-dir-%d/", i)
expectedObjects = append(expectedObjects, &pb.Object{Key: keyNestedDir, IsDir: true})
keyEmptyDir := fmt.Sprintf("list-objects-paginated/empty-dir-%d/", i)
expectedObjects = append(expectedObjects, &pb.Object{Key: keyEmptyDir, IsDir: true})
keyNestedEmptyDir := fmt.Sprintf("list-objects-paginated/empty-dir-%d/dir-%d/", i, i)
expectedObjects = append(expectedObjects, &pb.Object{Key: keyNestedEmptyDir, IsDir: true})
}
r.ElementsMatch(resp.Objects, expectedObjects)
})
// Test Scenario 4: Edge cases
t.Run("EdgeCases", func(t *testing.T) {
// Subtest for a prefix that doesn't exist
t.Run("NonExistentPrefix", func(t *testing.T) {
r := require.New(t)
t.Logf("testing ListObjects for a non-existent prefix")
req := &pb.ListObjectsRequest{
Bucket: bucket,
KeyPrefix: "this/prefix/does/not/exist/",
Recursive: false,
}
resp, err := p.ListObjects(ctx, req)
r.NoError(err)
r.NotNil(resp)
r.Empty(resp.Objects)
})
// Subtest for listing inside a known empty directory
t.Run("InsideEmptyDirectory", func(t *testing.T) {
r := require.New(t)
t.Logf("testing ListObjects inside an empty directory")
req := &pb.ListObjectsRequest{
Bucket: bucket,
KeyPrefix: "list-objects-paginated/empty-dirs/dir-10/",
Recursive: false,
}
resp, err := p.ListObjects(ctx, req)
r.NoError(err)
r.NotNil(resp)
r.Empty(resp.Objects)
})
})
}
+21 -14
View File
@@ -1,24 +1,20 @@
# Copyright IBM Corp. 2021, 2026
# SPDX-License-Identifier: MPL-2.0
variable "iam_user_prefix" {
default = "boundary-plugin-host-aws-test-iam-user"
}
variable "iam_user_count" {
default = 6
}
resource "random_id" "aws_iam_user_name" {
count = var.iam_user_count
prefix = "${var.iam_user_prefix}-${count.index}"
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
byte_length = 4
}
resource "aws_iam_user" "user" {
count = var.iam_user_count
name = random_id.aws_iam_user_name[count.index].dec
force_destroy = true
count = var.iam_user_count
name = random_id.aws_iam_user_name[count.index].dec
force_destroy = true
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
tags = {
"boundary-demo" = local.hashicorp_email
}
}
resource "aws_iam_access_key" "user_initial_key" {
@@ -26,8 +22,13 @@ resource "aws_iam_access_key" "user_initial_key" {
user = aws_iam_user.user[count.index].name
}
resource "random_id" "aws_ec2_policy_name" {
prefix = "BoundaryPluginHost"
byte_length = 4
}
resource "aws_iam_policy" "ec2_describeinstances" {
name = "BoundaryPluginHostAwsTestDescribeInstancesPolicy"
name = random_id.aws_ec2_policy_name.dec
policy = <<EOF
{
@@ -51,9 +52,15 @@ resource "aws_iam_user_policy_attachment" "user_ec2_describeinstances_attachment
policy_arn = aws_iam_policy.ec2_describeinstances.arn
}
resource "random_id" "aws_iam_policy_name" {
count = var.iam_user_count
prefix = "BoundaryPluginCredentials"
byte_length = 4
}
resource "aws_iam_policy" "user_self_manage_policy" {
count = var.iam_user_count
name = "BoundaryPluginHostAwsTestUserSelfManagePolicy-${count.index}"
name = random_id.aws_iam_policy_name[count.index].dec
policy = <<EOF
{
+1 -1
View File
@@ -5,7 +5,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
version = "~> 6.0"
}
}
}
+49
View File
@@ -0,0 +1,49 @@
# Copyright IBM Corp. 2021, 2026
# SPDX-License-Identifier: MPL-2.0
data "aws_caller_identity" "current" {}
variable "iam_user_count" {
default = 6
}
# We are using a shared account for testing so make sure the stale ec2
# machine tags don't match for this test run
resource "random_id" "foo_key" {
prefix = "foo"
byte_length = 4
}
resource "random_id" "bar_key" {
prefix = "bar"
byte_length = 4
}
resource "random_id" "baz_key" {
prefix = "baz"
byte_length = 4
}
locals {
hashicorp_email = split(":", data.aws_caller_identity.current.user_id)[1]
instance_tags = [
{
"${random_id.foo_key.dec}" = "true"
},
{
"${random_id.foo_key.dec}" = "true"
"${random_id.bar_key.dec}" = "true"
},
{
"${random_id.bar_key.dec}" = "true"
},
{
"${random_id.bar_key.dec}" = "true"
"${random_id.baz_key.dec}" = "true"
},
{
"${random_id.baz_key.dec}" = "true"
},
]
}
+11 -25
View File
@@ -1,28 +1,6 @@
# Copyright IBM Corp. 2021, 2026
# SPDX-License-Identifier: MPL-2.0
variable "instance_tags" {
default = [
{
"foo" = "true",
},
{
"foo" = "true",
"bar" = "true",
},
{
"bar" = "true",
},
{
"bar" = "true",
"baz" = "true",
},
{
"baz" = "true",
},
]
}
data "aws_availability_zones" "azs" {}
data "aws_ami" "ubuntu" {
@@ -52,12 +30,12 @@ resource "aws_subnet" "subnet" {
}
resource "aws_instance" "instances" {
count = length(var.instance_tags)
count = length(local.instance_tags)
ami = data.aws_ami.ubuntu.id
instance_type = "t3.nano"
subnet_id = aws_subnet.subnet.id
tags = var.instance_tags[count.index]
tags = local.instance_tags[count.index]
}
output "instance_ids" {
@@ -74,4 +52,12 @@ output "instance_tags" {
value = {
for _, r in aws_instance.instances : r.id => r.tags
}
}
}
output "instance_tag_keys" {
value = [
random_id.foo_key.dec,
random_id.bar_key.dec,
random_id.baz_key.dec,
]
}
+22 -20
View File
@@ -34,10 +34,18 @@ resource "aws_iam_policy" "valid" {
"s3:GetObject",
"s3:DeleteObject",
"s3:GetObjectAttributes",
"s3:ListBucket"
"s3:ListBucket",
"s3:DeleteObject"
],
"Effect": "Allow",
"Resource": "${aws_s3_bucket.test.arn}*"
},
{
"Action": [
"s3:ListBucket"
],
"Effect": "Allow",
"Resource": "${aws_s3_bucket.test.arn}"
}
]
@@ -196,7 +204,7 @@ resource "aws_iam_role_policy_attachment" "missing_delete_obj" {
# Static Credential Testing
resource "random_id" "user_name" {
count = var.iam_user_count
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
prefix = "demo-${local.hashicorp_email}" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
byte_length = 4
}
@@ -204,10 +212,8 @@ resource "aws_iam_user" "test" {
count = var.iam_user_count
name = random_id.user_name[count.index].dec
force_destroy = true
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
tags = {
"boundary-demo" = local.hashicorp_email
}
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
tags = local.tags
}
resource "aws_iam_access_key" "test" {
@@ -277,17 +283,15 @@ resource "aws_iam_user_policy_attachment" "missing_delete_obj" {
# Edge case for missing GetObject permission
resource "random_id" "missing_get_obj" {
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
prefix = "demo-${local.hashicorp_email}" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
byte_length = 4
}
resource "aws_iam_user" "missing_get_obj" {
name = random_id.missing_get_obj.dec
force_destroy = true
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
tags = {
"boundary-demo" = local.hashicorp_email
}
name = random_id.missing_get_obj.dec
force_destroy = true
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
tags = local.tags
}
resource "aws_iam_access_key" "missing_get_obj" {
@@ -302,17 +306,15 @@ resource "aws_iam_user_policy_attachment" "missing_get_obj" {
# Edge case for missing PutObject permission
resource "random_id" "missing_put_obj" {
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
prefix = "demo-${local.hashicorp_email}" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
byte_length = 4
}
resource "aws_iam_user" "missing_put_obj" {
name = random_id.missing_put_obj.dec
force_destroy = true
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser"
tags = {
"boundary-demo" = local.hashicorp_email
}
name = random_id.missing_put_obj.dec
force_destroy = true
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
tags = local.tags
}
resource "aws_iam_access_key" "missing_put_obj" {
+1 -1
View File
@@ -5,7 +5,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
version = "~> 6.0"
}
}
}
+10 -5
View File
@@ -6,19 +6,19 @@ output "bucket_name" {
}
output "valid_policy_arn" {
value = aws_iam_role.valid.arn
value = aws_iam_role.valid.arn
}
output "missing_put_obj_policy_arn" {
value = aws_iam_role.missing_put_obj.arn
value = aws_iam_role.missing_put_obj.arn
}
output "missing_get_obj_policy_arn" {
value = aws_iam_role.missing_get_obj.arn
value = aws_iam_role.missing_get_obj.arn
}
output "missing_delete_obj_policy_arn" {
value = aws_iam_role.missing_delete_obj.arn
value = aws_iam_role.missing_delete_obj.arn
}
output "iam_user_names" {
@@ -67,4 +67,9 @@ output "iam_access_key_missing_delete_obj" {
output "iam_secret_access_key_missing_delete_obj" {
value = aws_iam_access_key.missing_delete_obj.secret
sensitive = true
}
}
output "s3_pagination_test_object_count" {
description = "The number of S3 objects created for each pagination test scenario."
value = local.s3_pagination_test_object_count
}
+10 -2
View File
@@ -9,9 +9,17 @@ resource "aws_s3_bucket" "test" {
resource "aws_s3_bucket_public_access_block" "secure_access" {
bucket = aws_s3_bucket.test.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
}
resource "aws_s3_object" "s3_files" {
for_each = local.s3_files
bucket = aws_s3_bucket.test.id
key = each.key
content = each.value
tags = local.tags
}
+41 -1
View File
@@ -16,7 +16,47 @@ locals {
hashicorp_email = split(":", data.aws_caller_identity.current.user_id)[1]
tags = {
User = split(":", data.aws_caller_identity.current.user_id)[1]
Repo = "boundary-plugin-aws"
Repo = "boundary-plugin-aws"
Environment = "Demo"
}
# total number of objects of each type to create
s3_pagination_test_object_count = 350
# Dynamically build a flat map of S3 objects:
# - root-level text files
# - nested JSON files
# - placeholder "empty directories"
# - some static files in list-objects directory
# This map is consumed directly by an aws_s3_object resource
s3_files = merge(
# Root-level text files
{
for i in range(local.s3_pagination_test_object_count) :
"list-objects-paginated/root-file-${i}.txt" => "This is a root-level file #${i}."
},
# Nested JSON files inside sub-directories
{
for i in range(local.s3_pagination_test_object_count) :
"list-objects-paginated/nested-dir-${i}/data-file-${i}.json" =>
jsonencode({
id = i,
message = "This is a nested JSON file #${i}."
})
},
# "Empty" directories (in S3 these are just placeholder objects ending with `/`)
{
for i in range(local.s3_pagination_test_object_count) :
"list-objects-paginated/empty-dir-${i}/dir-${i}/" => "" # zero-length object to mimic directory
},
# Static example files in list-objects directory
{
"list-objects/file1.txt" = "This is the content for file 1."
"list-objects/data.json" = jsonencode({ key = "value", items = [1, 2, 3] })
"list-objects/nested-dir/file2.txt" = "This is a nested file."
"list-objects/empty-nested-dir/" = ""
}
)
}