Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
241901319d | ||
|
|
56f89a5dcf | ||
|
|
5b7b669b41 | ||
|
|
7b4ee7f403 | ||
|
|
916324ee64 | ||
|
|
11d3d6d523 | ||
|
|
4758d5d9cd | ||
|
|
42e2126217 | ||
|
|
5d098ac71d | ||
|
|
949976ec19 | ||
|
|
49f692e7a8 | ||
|
|
a75ac95465 | ||
|
|
7fd6d3f3e5 | ||
|
|
bdd27e2562 | ||
|
|
f17f05ec82 | ||
|
|
899c62ce06 | ||
|
|
ce22c1c46a | ||
|
|
2f9163ad0e | ||
|
|
7caff84c06 | ||
|
|
5f96422fbf | ||
|
|
4dac4498d3 | ||
|
|
9af0110945 | ||
|
|
c2e7c94f9c | ||
|
|
5add0a1b53 |
No files matched your search
@@ -0,0 +1,11 @@
|
||||
## PCI review checklist
|
||||
|
||||
<!-- heimdall_github_prtemplate:grc-pci_dss-2024-01-05 -->
|
||||
|
||||
- [ ] I have documented a clear reason for, and description of, the change I am making.
|
||||
|
||||
- [ ] If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.
|
||||
|
||||
- [ ] If applicable, I've documented the impact of any changes to security controls.
|
||||
|
||||
Examples of changes to security controls include using new access control methods, adding or removing logging pipelines, etc.
|
||||
@@ -4,3 +4,12 @@ plan.tfplan
|
||||
*.log
|
||||
*.out
|
||||
*.html
|
||||
|
||||
# IntelliJ IDEA project files
|
||||
.idea
|
||||
*.iprg
|
||||
|
||||
.DS_Store
|
||||
|
||||
.env
|
||||
.envrc
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo, unless a later match takes precedence.
|
||||
|
||||
@hashicorp/boundary
|
||||
|
||||
* @hashicorp/boundary
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2021 HashiCorp, Inc.
|
||||
Copyright IBM Corp. 2021, 2026
|
||||
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
@@ -1,66 +1,69 @@
|
||||
module github.com/hashicorp/boundary-plugin-aws
|
||||
|
||||
go 1.21
|
||||
go 1.25.7
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.20.1
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.22.2
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.38.2
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.21.2
|
||||
github.com/aws/smithy-go v1.14.1
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/hashicorp/boundary/sdk v0.0.43-0.20240717182311-a20aae98794a
|
||||
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.59
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0
|
||||
github.com/hashicorp/terraform-json v0.14.0
|
||||
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.8.4
|
||||
google.golang.org/grpc v1.61.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
google.golang.org/grpc v1.79.3
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.39 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.13.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.2 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // 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.15.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // 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.5.0 // indirect
|
||||
github.com/hashicorp/go-kms-wrapping/v2 v2.0.14 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // 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.7 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/go-version v1.5.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // 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
|
||||
github.com/mitchellh/pointerstructure v1.2.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/zclconf/go-cty v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/zclconf/go-cty v1.15.0 // 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
|
||||
)
|
||||
@@ -4,168 +4,168 @@ github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7Y
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||
github.com/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.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.20.1 h1:rZBf5DWr7YGrnlTK4kgDQGn1ltqOg5orCYb/UhOFZkg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.20.1/go.mod h1:NU06lETsFm8fUC6ZjhgDpVBcGZTFQ6XM+LZWZxMI4ac=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12 h1:lN6L3LrYHeZ6xCxaIYtoWCx4GMLk4nRknsh29OMSqHY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12/go.mod h1:TDCkEAkMTXxTs0oLBGBKpBZbk3NLh8EvAfF0Q3x8/0c=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.33 h1:JKcw5SFxFW/rpM4mOPjv0VQ11E2kxW13F3exWOy7VZU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.33/go.mod h1:hXO/l9pgY3K5oZJldamP0pbZHdPqqk+4/maa7DSD3cA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.32 h1:lIH1eKPcCY1ylR4B6PkBGRWMHO3aVenOKJHWiS4/G2w=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.32/go.mod h1:lL8U3v/Y79YRG69WlAho0OHIKUXCyFvSXaIvfo81sls=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.8 h1:DK/9C+UN/X+1+Wm8pqaDksQr2tSLzq+8X1/rI/ZxKEQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.8/go.mod h1:ce7BgLQfYr5hQFdy67oX2svto3ufGtm6oBvmsHScI1Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38 h1:c8ed/T9T2K5I+h/JzmF5tpI46+OODQ74dzmdo+QnaMg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38/go.mod h1:qggunOChCMu9ZF/UkAfhTz25+U2rLVb3ya0Ua6TTfCA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.32 h1:hNeAAymUY5gu11WrrmFb3CVIp9Dar9hbo44yzzcQpzA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.32/go.mod h1:0ZXSqrty4FtQ7p8TEuRde/SZm9X05KT18LAUlR40Ln0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.39 h1:fc0ukRAiP1syoSGZYu+DaE+FulSYhTiJ8WpVu5jElU4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.39/go.mod h1:WLAW8PT7+JhjZfLSWe7WEJaJu0GNo0cKc2Zyo003RBs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.1 h1:vUh7dBFNS3oFCtVv6CiYKh5hP9ls8+kIpKLeFruIBLk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.1/go.mod h1:sFMeinkhj/SZKQM8BxtvNtSPjJEo0Xrz+w3g2e4FSKI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0 h1:NXi4pNJWjAaiI56P1Rl8DC9A4jMNRE00WNBsDua5WRg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0/go.mod h1:L3ZT0N/vBsw77mOAawXmRnREpEjcHd2v5Hzf7AkIH8M=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.22.2 h1:DPFxx/6Zwes/MiadlDteVqDKov7yQ5v9vuwfhZuJm1s=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.22.2/go.mod h1:cQTMNdo/Z5t1DDRsUnx0a2j6cPnytMBidUYZw2zks28=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13 h1:iV/W5OMBys+66OeXJi/7xIRrKZNsu0ylsLGu+6nbmQE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13/go.mod h1:ReJb6xYmtGyu9KoFtRreWegbN9dZqvZIIv4vWnhcsyI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.33 h1:QviNkc+vGSuEHx8P+pVNKOdWLXBPIwMFv7p0fphgE4U=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.33/go.mod h1:fABTUmOrAgAalG2i9WJpjBvlnk7UK8YmnYaxN+Q2CwE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.32 h1:dGAseBFEYxth10V23b5e2mAS+tX7oVbfYHD6dnDdAsg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.32/go.mod h1:4jwAWKEkCR0anWk5+1RbfSg1R5Gzld7NLiuaq5bTR/Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.1 h1:PT6PBCycRwhpEW5hJnRiceCeoWJ+r3bdgXtV+VKG7Pk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.1/go.mod h1:TqoxCLwT2nrxrBGA+z7t6OWM7LBkgRckK3gOjYE+7JA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.38.2 h1:v346f1h8sUBKXnEbrv43L37MTBlFHyKXQPIZHNAaghA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.38.2/go.mod h1:cwCATiyNrXK9P2FsWdZ89g9mpsYv2rhk0UA/KByl5fY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.13.2 h1:A2RlEMo4SJSwbNoUUgkxTAEMduAy/8wG3eB2b2lP4gY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.13.2/go.mod h1:ju+nNXUunfIFamXUIZQiICjnO/TPlOmWcYhZcSy7xaE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.2 h1:OJELEgyaT2kmaBGZ+myyZbTTLobfe3ox3FSh5eYK9Qs=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.2/go.mod h1:ubDBBaDFs1GHijSOTi8ljppML15GLG0HxhILtbjNNYQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.21.2 h1:ympg1+Lnq33XLhcK/xTG4yZHPs1Oyxu+6DEWbl7qOzA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.21.2/go.mod h1:FQ/DQcOfESELfJi5ED+IPPAjI5xC6nxtSolVVB773jM=
|
||||
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/aws/smithy-go v1.14.1 h1:EFKMUmH/iHMqLiwoEDx2rRjRQpI1YCn5jTysoaDujFs=
|
||||
github.com/aws/smithy-go v1.14.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
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=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
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/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
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.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/boundary/sdk v0.0.43-0.20240717182311-a20aae98794a h1:SwVze6sYE5o+J9qdcgj29auY1k6O8oSo9DC2+2Gb9rw=
|
||||
github.com/hashicorp/boundary/sdk v0.0.43-0.20240717182311-a20aae98794a/go.mod h1:9iOT7kDM6mYcSkKxNuZlv8rP7U5BG1kXoevjLLL8lNQ=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
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.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.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.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.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.5 h1:jrnDfQm2hCQ0/hEselgqzV4fK16gpZoY0OWGZpVPNHM=
|
||||
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.5/go.mod h1:psh1qKep5ukvuNobFY/hCybuudlkkACpmazOsCgX5Rg=
|
||||
github.com/hashicorp/go-kms-wrapping/v2 v2.0.14 h1:1ZuhfnZgRnLK8S0KovJkoTCRIQId5pv3sDR7pG5VQBw=
|
||||
github.com/hashicorp/go-kms-wrapping/v2 v2.0.14/go.mod h1:0dWtzl2ilqKpavgM3id/kFK9L3tjo6fS4OhbVPSYpnQ=
|
||||
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.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.5.2 h1:aWv8eimFqWlsEiMrYZdPYl+FdHaBJSN4AWwGWfT1G2Y=
|
||||
github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0 h1:ca5TSI4AgaOncPpyzLDtCGjVEtKukONpeM95vFxXCOQ=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.0.0/go.mod h1:7CUvZtfTp2U0CYQCLzMtS2ngckjAZePSfwrE2aeDP1M=
|
||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
|
||||
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.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/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0=
|
||||
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/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.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.5 h1:dvk7TIXCZpmfOlM+9mlcrWmWjw/wlKT+VDq2wMvfPJU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.5/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.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E=
|
||||
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/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.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e17dKDpqV7s=
|
||||
github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM=
|
||||
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/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.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/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
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/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
@@ -179,107 +179,60 @@ 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=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
|
||||
github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0=
|
||||
github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
|
||||
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.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
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-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/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.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-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=
|
||||
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=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
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.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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/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=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg=
|
||||
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
|
||||
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
|
||||
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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-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.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.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/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,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -37,7 +37,7 @@ type CredentialAttributes struct {
|
||||
// CredentialsConfig used for configuring an AWS session. An status error is returned
|
||||
// with an InvalidArgument code if any unrecognized fields are found in the protobuf
|
||||
// struct input.
|
||||
func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, required bool) (*awsutil.CredentialsConfig, error) {
|
||||
func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, dualStack bool) (*awsutil.CredentialsConfig, error) {
|
||||
// initialize secrets if it is nil
|
||||
// secrets can be nil because static credentials are optional
|
||||
if secrets == nil {
|
||||
@@ -49,13 +49,13 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
|
||||
unknownFields := values.StructFields(secrets)
|
||||
badFields := make(map[string]string)
|
||||
|
||||
accessKey, err := values.GetStringValue(secrets, ConstAccessKeyId, required)
|
||||
accessKey, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = err.Error()
|
||||
}
|
||||
delete(unknownFields, ConstAccessKeyId)
|
||||
|
||||
secretKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, required)
|
||||
secretKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error()
|
||||
}
|
||||
@@ -117,6 +117,17 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
|
||||
opts = append(opts, awsutil.WithRoleTags(attrs.RoleTags))
|
||||
}
|
||||
|
||||
if dualStack {
|
||||
opts = append(opts, []awsutil.Option{
|
||||
awsutil.WithIamEndpointResolver(&iamEndpointResolver{
|
||||
dualStack: dualStack,
|
||||
}),
|
||||
awsutil.WithStsEndpointResolver(&stsEndpointResolver{
|
||||
dualStack: dualStack,
|
||||
}),
|
||||
}...)
|
||||
}
|
||||
|
||||
cfg, err := awsutil.NewCredentialsConfig(opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus(err.Error())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -105,8 +105,8 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
secrets *structpb.Struct
|
||||
required bool
|
||||
attrs *CredentialAttributes
|
||||
dualStack bool
|
||||
expected *awsutil.CredentialsConfig
|
||||
expectedErrContains string
|
||||
}{
|
||||
@@ -137,30 +137,22 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing access key",
|
||||
name: "with dualstack",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
dualStack: true,
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
required: true,
|
||||
expectedErrContains: "secrets.access_key_id: missing required value",
|
||||
},
|
||||
{
|
||||
name: "missing secret key",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
||||
},
|
||||
expected: &awsutil.CredentialsConfig{
|
||||
AccessKey: "AKIAfoobar",
|
||||
SecretKey: "bazqux",
|
||||
Region: "us-west-2",
|
||||
},
|
||||
required: true,
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expectedErrContains: "secrets.secret_access_key: missing required value",
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
@@ -260,7 +252,7 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
actual, err := GetCredentialsConfig(tc.secrets, tc.attrs, tc.required)
|
||||
actual, err := GetCredentialsConfig(tc.secrets, tc.attrs, tc.dualStack)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
@@ -276,6 +268,13 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
require.Equal(tc.expected.RoleExternalId, actual.RoleExternalId)
|
||||
require.Equal(tc.expected.RoleSessionName, actual.RoleSessionName)
|
||||
require.Equal(tc.expected.RoleTags, actual.RoleTags)
|
||||
if tc.dualStack {
|
||||
require.NotNil(actual.IAMEndpointResolver)
|
||||
require.NotNil(actual.STSEndpointResolver)
|
||||
} else {
|
||||
require.Nil(actual.IAMEndpointResolver)
|
||||
require.Nil(actual.STSEndpointResolver)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -11,7 +11,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
smithyEndpoints "github.com/aws/smithy-go/endpoints"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
||||
@@ -114,7 +117,7 @@ func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
|
||||
return errors.BadRequestStatus("missing credentials config")
|
||||
}
|
||||
if _, err := s.CredentialsConfig.GetCallerIdentity(ctx, s.testOpts...); err != nil {
|
||||
st, _ := errors.ParseAWSError(err, "validating credentials")
|
||||
st, _ := errors.ParseAWSError("validating credentials", err)
|
||||
return st.Err()
|
||||
}
|
||||
return nil
|
||||
@@ -135,7 +138,7 @@ func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error {
|
||||
if err := s.CredentialsConfig.RotateKeys(ctx, append([]awsutil.Option{
|
||||
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
|
||||
}, s.testOpts...)...); err != nil {
|
||||
st, _ := errors.ParseAWSError(err, "error rotating credentials")
|
||||
st, _ := errors.ParseAWSError("rotating credentials", err)
|
||||
return st.Err()
|
||||
}
|
||||
s.CredsLastRotatedTime = time.Now()
|
||||
@@ -192,7 +195,7 @@ func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Otherwise treat it as an actual error.
|
||||
st, _ := errors.ParseAWSError(err, "failed to delete access key")
|
||||
st, _ := errors.ParseAWSError("deleting credentials", err)
|
||||
return st.Err()
|
||||
}
|
||||
|
||||
@@ -203,13 +206,7 @@ func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error {
|
||||
|
||||
// GenerateCredentialChain returns a AWS configuration for the credentials in the state.
|
||||
func (s *AwsCredentialPersistedState) GenerateCredentialChain(ctx context.Context) (*aws.Config, error) {
|
||||
// Default `WithSharedCredentials` is `true` - This disables the ability to
|
||||
// read credentials from environment variables because we set the default
|
||||
// profile options, so we need to set it to false (see
|
||||
// awsutil@v2.(*CredentialsConfig).generateAwsConfigOptions) for extra
|
||||
// details.
|
||||
opts := append(s.testOpts, awsutil.WithSharedCredentials(false))
|
||||
return s.CredentialsConfig.GenerateCredentialChain(ctx, opts...)
|
||||
return s.CredentialsConfig.GenerateCredentialChain(ctx, s.testOpts...)
|
||||
}
|
||||
|
||||
// ToMap returns a map of the credentials stored in the persisted state.
|
||||
@@ -237,7 +234,7 @@ func (s *AwsCredentialPersistedState) ToMap() map[string]any {
|
||||
|
||||
// AwsCredentialPersistedStateFromProto parses values out of a protobuf struct input
|
||||
// and returns a AwsCredentialPersistedState used for configuring an AWS session.
|
||||
func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
|
||||
func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, dualStack bool, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
|
||||
// initialize secrets if it is nil
|
||||
// secrets can be nil because static credentials are optional
|
||||
if secrets == nil {
|
||||
@@ -252,17 +249,17 @@ func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *Crede
|
||||
|
||||
accessKeyId, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
return nil, fmt.Errorf("persisted state integrity: %w", err)
|
||||
}
|
||||
|
||||
secretAccessKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
return nil, fmt.Errorf("persisted state integrity: %w", err)
|
||||
}
|
||||
|
||||
credsLastRotatedTime, err := values.GetTimeValue(secrets, ConstCredsLastRotatedTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||
return nil, fmt.Errorf("persisted state integrity: %w", err)
|
||||
}
|
||||
|
||||
s, err := NewAwsCredentialPersistedState(opts...)
|
||||
@@ -292,6 +289,17 @@ func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *Crede
|
||||
if len(attrs.RoleTags) != 0 {
|
||||
awsOpts = append(awsOpts, awsutil.WithRoleTags(attrs.RoleTags))
|
||||
}
|
||||
if dualStack {
|
||||
awsOpts = append(awsOpts, []awsutil.Option{
|
||||
awsutil.WithIamEndpointResolver(&iamEndpointResolver{
|
||||
dualStack: dualStack,
|
||||
}),
|
||||
awsutil.WithStsEndpointResolver(&stsEndpointResolver{
|
||||
dualStack: dualStack,
|
||||
}),
|
||||
}...)
|
||||
}
|
||||
|
||||
credentialsConfig, err := awsutil.NewCredentialsConfig(awsOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -321,3 +329,23 @@ func GetCredentialType(cc *awsutil.CredentialsConfig) CredentialType {
|
||||
|
||||
return Unknown
|
||||
}
|
||||
|
||||
type iamEndpointResolver struct {
|
||||
dualStack bool
|
||||
}
|
||||
|
||||
func (e *iamEndpointResolver) ResolveEndpoint(ctx context.Context, params iam.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) {
|
||||
params.UseDualStack = aws.Bool(e.dualStack)
|
||||
resolver, err = iam.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
|
||||
return
|
||||
}
|
||||
|
||||
type stsEndpointResolver struct {
|
||||
dualStack bool
|
||||
}
|
||||
|
||||
func (e *stsEndpointResolver) ResolveEndpoint(ctx context.Context, params sts.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) {
|
||||
params.UseDualStack = aws.Bool(e.dualStack)
|
||||
resolver, err = sts.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
|
||||
return
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -112,6 +112,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
name string
|
||||
secrets *structpb.Struct
|
||||
attrs *CredentialAttributes
|
||||
dualStack bool
|
||||
opts []AwsCredentialPersistedStateOption
|
||||
expected *AwsCredentialPersistedState
|
||||
expectedErr string
|
||||
@@ -174,6 +175,26 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with dualstack static credentials",
|
||||
secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstAccessKeyId: structpb.NewStringValue("foobar"),
|
||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||
},
|
||||
},
|
||||
dualStack: true,
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expected: &AwsCredentialPersistedState{
|
||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||
Region: "us-west-2",
|
||||
AccessKey: "foobar",
|
||||
SecretKey: "bazqux",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad last rotated time",
|
||||
secrets: &structpb.Struct{
|
||||
@@ -186,7 +207,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
attrs: &CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
expectedErr: "persisted state integrity error: could not parse time in value \"creds_last_rotated_time\": parsing time \"notatime\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"notatime\" as \"2006\"",
|
||||
expectedErr: "persisted state integrity: could not parse time in value \"creds_last_rotated_time\": parsing time \"notatime\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"notatime\" as \"2006\"",
|
||||
},
|
||||
{
|
||||
name: "option error",
|
||||
@@ -318,7 +339,7 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
actual, err := AwsCredentialPersistedStateFromProto(tc.secrets, tc.attrs, tc.opts...)
|
||||
actual, err := AwsCredentialPersistedStateFromProto(tc.secrets, tc.attrs, tc.dualStack, tc.opts...)
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
@@ -336,6 +357,13 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
|
||||
require.Equal(tc.expected.CredentialsConfig.RoleExternalId, actual.CredentialsConfig.RoleExternalId)
|
||||
require.Equal(tc.expected.CredentialsConfig.RoleSessionName, actual.CredentialsConfig.RoleSessionName)
|
||||
require.Equal(tc.expected.CredentialsConfig.RoleTags, actual.CredentialsConfig.RoleTags)
|
||||
if tc.dualStack {
|
||||
require.NotNil(actual.CredentialsConfig.STSEndpointResolver)
|
||||
require.NotNil(actual.CredentialsConfig.IAMEndpointResolver)
|
||||
} else {
|
||||
require.Nil(actual.CredentialsConfig.STSEndpointResolver)
|
||||
require.Nil(actual.CredentialsConfig.IAMEndpointResolver)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -458,7 +486,7 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedErr: "aws service unknown: unknown error: error rotating credentials",
|
||||
expectedErr: "aws service unknown: unknown error: rotating credentials",
|
||||
expectedStatusCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -568,7 +596,7 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
|
||||
},
|
||||
},
|
||||
credentialConfig: &awsutil.CredentialsConfig{},
|
||||
expectedErr: "aws service unknown: unknown error: failed to delete access key",
|
||||
expectedErr: "aws service unknown: unknown error: deleting credentials",
|
||||
expectedStatusCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
@@ -693,7 +721,7 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedErr: "aws service unknown: unknown error: failed to delete access key",
|
||||
expectedErr: "aws service unknown: unknown error: deleting credentials",
|
||||
expectedStatusCode: codes.Unknown,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
+88
-24
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package errors
|
||||
@@ -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"
|
||||
@@ -81,11 +86,12 @@ func InvalidArgumentError(msg string, f map[string]string) error {
|
||||
// and credentials. This method will fallback to parsing the http status code
|
||||
// when it cannot match an aws error code. This method does not handle specific
|
||||
// service type errors such as S3 or EC2.
|
||||
func ParseAWSError(err error, msg string) (st *status.Status, permission *pb.Permission) {
|
||||
func ParseAWSError(op string, err error) (st *status.Status, permission *pb.Permission) {
|
||||
if err == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("%s: %v", op, err)
|
||||
// find the service name of the aws api
|
||||
serviceName := "unknown"
|
||||
var oe *smithy.OperationError
|
||||
@@ -113,11 +119,54 @@ func ParseAWSError(err error, msg string) (st *status.Status, permission *pb.Per
|
||||
Codes: retry.DefaultThrottleErrorCodes,
|
||||
}
|
||||
if throttleErr.IsErrorThrottle(err).Bool() {
|
||||
statusMsg := fmt.Sprintf("aws service %s: throttling error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: throttling: %s", serviceName, msg)
|
||||
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, ¬FoundErr) {
|
||||
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() {
|
||||
@@ -126,37 +175,40 @@ func ParseAWSError(err error, msg string) (st *status.Status, permission *pb.Per
|
||||
case awsErrorInvalidAccessKeyId:
|
||||
fallthrough
|
||||
case awsErrorExpiredToken:
|
||||
statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg)
|
||||
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: apiErr.ErrorMessage(),
|
||||
CheckedAt: timestamppb.Now(),
|
||||
}
|
||||
case awsErrorNoSuchBucket:
|
||||
statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.NotFound, statusMsg), &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: apiErr.ErrorMessage(),
|
||||
CheckedAt: timestamppb.Now(),
|
||||
}
|
||||
case awsErrorBadDigest:
|
||||
statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.Aborted, statusMsg), nil
|
||||
case awsErrorNoSuchKey:
|
||||
fallthrough
|
||||
case awsErrorNotFound:
|
||||
fallthrough
|
||||
case awsErrorInvalidObjectState:
|
||||
statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.NotFound, statusMsg), nil
|
||||
case awsErrorRequestTimeout:
|
||||
fallthrough
|
||||
case awsErrorRequestTimeoutException:
|
||||
statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
||||
return status.New(codes.DeadlineExceeded, statusMsg), nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -165,39 +217,39 @@ func ParseAWSError(err error, msg string) (st *status.Status, permission *pb.Per
|
||||
defer httpErr.Response.Body.Close()
|
||||
switch httpErr.HTTPStatusCode() {
|
||||
case http.StatusBadRequest:
|
||||
statusMsg := fmt.Sprintf("aws service %s: bad request error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: bad request: %s", serviceName, msg)
|
||||
return status.New(codes.InvalidArgument, statusMsg), nil
|
||||
case http.StatusUnauthorized:
|
||||
statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg)
|
||||
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: buf.String(),
|
||||
CheckedAt: timestamppb.Now(),
|
||||
}
|
||||
case http.StatusForbidden:
|
||||
statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg)
|
||||
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: buf.String(),
|
||||
CheckedAt: timestamppb.Now(),
|
||||
}
|
||||
case http.StatusNotFound:
|
||||
statusMsg := fmt.Sprintf("aws service %s: resource not found error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: resource not found: %s", serviceName, msg)
|
||||
return status.New(codes.NotFound, statusMsg), nil
|
||||
case http.StatusTooManyRequests:
|
||||
statusMsg := fmt.Sprintf("aws service %s: throttling error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: throttling: %s", serviceName, msg)
|
||||
return status.New(codes.Unavailable, statusMsg), nil
|
||||
case http.StatusInternalServerError:
|
||||
fallthrough
|
||||
case http.StatusBadGateway:
|
||||
fallthrough
|
||||
case http.StatusServiceUnavailable:
|
||||
statusMsg := fmt.Sprintf("aws service %s: connectivity error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: connectivity: %s", serviceName, msg)
|
||||
return status.New(codes.Unavailable, statusMsg), nil
|
||||
case http.StatusRequestTimeout:
|
||||
fallthrough
|
||||
case http.StatusGatewayTimeout:
|
||||
statusMsg := fmt.Sprintf("aws service %s: timeout error: %s", serviceName, msg)
|
||||
statusMsg := fmt.Sprintf("aws service %s: timeout: %s", serviceName, msg)
|
||||
return status.New(codes.DeadlineExceeded, statusMsg), nil
|
||||
}
|
||||
}
|
||||
@@ -205,14 +257,26 @@ func ParseAWSError(err error, msg string) (st *status.Status, permission *pb.Per
|
||||
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()
|
||||
}
|
||||
+116
-35
@@ -1,3 +1,6 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
@@ -41,7 +44,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-bad-request",
|
||||
err: TestAwsHttpResponseError(http.StatusBadRequest, "bad request"),
|
||||
expectedStatusCode: codes.InvalidArgument,
|
||||
expectedStatusMsg: "aws service unknown: bad request error: test",
|
||||
expectedStatusMsg: "aws service unknown: bad request: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -50,7 +53,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-unauthorized",
|
||||
err: TestAwsHttpResponseError(http.StatusUnauthorized, "unauthorized request"),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: "unauthorized request",
|
||||
@@ -61,7 +64,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-forbidden",
|
||||
err: TestAwsHttpResponseError(http.StatusForbidden, "forbidden request"),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: "forbidden request",
|
||||
@@ -72,7 +75,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-not-found",
|
||||
err: TestAwsHttpResponseError(http.StatusNotFound, "not found request"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws service unknown: resource not found error: test",
|
||||
expectedStatusMsg: "aws service unknown: resource not found: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -81,7 +84,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-too-many-requests",
|
||||
err: TestAwsHttpResponseError(http.StatusTooManyRequests, "too many requests"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -90,7 +93,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-request-timeout",
|
||||
err: TestAwsHttpResponseError(http.StatusRequestTimeout, "request timeout"),
|
||||
expectedStatusCode: codes.DeadlineExceeded,
|
||||
expectedStatusMsg: "aws service unknown: timeout error: test",
|
||||
expectedStatusMsg: "aws service unknown: timeout: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -99,7 +102,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-internal-server-error",
|
||||
err: TestAwsHttpResponseError(http.StatusInternalServerError, "internal server error"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: connectivity error: test",
|
||||
expectedStatusMsg: "aws service unknown: connectivity: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -108,7 +111,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-bad-gateway",
|
||||
err: TestAwsHttpResponseError(http.StatusBadGateway, "bad gateway"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: connectivity error: test",
|
||||
expectedStatusMsg: "aws service unknown: connectivity: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -117,7 +120,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-service-unavailable",
|
||||
err: TestAwsHttpResponseError(http.StatusServiceUnavailable, "service unavailable"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: connectivity error: test",
|
||||
expectedStatusMsg: "aws service unknown: connectivity: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -126,16 +129,27 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "http-status-code-gateway-timeout",
|
||||
err: TestAwsHttpResponseError(http.StatusGatewayTimeout, "gateway timeout"),
|
||||
expectedStatusCode: codes.DeadlineExceeded,
|
||||
expectedStatusMsg: "aws service unknown: timeout error: test",
|
||||
expectedStatusMsg: "aws service unknown: timeout: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
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"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -144,7 +158,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-throttling-exception",
|
||||
err: TestAwsError("ThrottlingException", "throttling exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -153,7 +167,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-throttled-exception",
|
||||
err: TestAwsError("ThrottledException", "throttled exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -162,7 +176,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-throttled-exception",
|
||||
err: TestAwsError("RequestThrottledException", "request throttled exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -171,7 +185,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-too-many-requests-exception",
|
||||
err: TestAwsError("TooManyRequestsException", "too many requests exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -180,7 +194,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-provisioned-throughput-exceeded-exception",
|
||||
err: TestAwsError("ProvisionedThroughputExceededException", "provisioned throughput exceeded exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -189,7 +203,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-transaction-in-progress-exception",
|
||||
err: TestAwsError("TransactionInProgressException", "transaction in progress exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -198,7 +212,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-limit-exceeded",
|
||||
err: TestAwsError("RequestLimitExceeded", "request limit exceeded"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -207,7 +221,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-bandwidth-limit-exceeded",
|
||||
err: TestAwsError("BandwidthLimitExceeded", "bandwidth limit exceeded"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -216,7 +230,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-limit-exceeded-exception",
|
||||
err: TestAwsError("LimitExceededException", "limit exceeded exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -225,7 +239,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-throttled",
|
||||
err: TestAwsError("RequestThrottled", "request throttled"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -234,7 +248,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-slow-down",
|
||||
err: TestAwsError("SlowDown", "slow down"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -243,7 +257,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-prior-request-not-complete",
|
||||
err: TestAwsError("PriorRequestNotComplete", "prior request not complete"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -252,7 +266,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-ec2-throttled-exception",
|
||||
err: TestAwsError("EC2ThrottledException", "ec2 throttled exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service unknown: throttling error: test",
|
||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -261,7 +275,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-timeout",
|
||||
err: TestAwsError("RequestTimeout", "request timeout"),
|
||||
expectedStatusCode: codes.DeadlineExceeded,
|
||||
expectedStatusMsg: "aws unknown error: test",
|
||||
expectedStatusMsg: "aws service unknown: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -270,7 +284,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-timeout-exception",
|
||||
err: TestAwsError("RequestTimeoutException", "request timeout exception"),
|
||||
expectedStatusCode: codes.DeadlineExceeded,
|
||||
expectedStatusMsg: "aws unknown error: test",
|
||||
expectedStatusMsg: "aws service unknown: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
@@ -279,7 +293,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-access-denied",
|
||||
err: TestAwsError(awsErrorAccessDenied, "not authorized to perform action"),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: "not authorized to perform action",
|
||||
@@ -290,7 +304,7 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-invalid-access-key-id",
|
||||
err: TestAwsError(awsErrorInvalidAccessKeyId, "The AWS Access Key Id you provided does not exist in our records."),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: "The AWS Access Key Id you provided does not exist in our records.",
|
||||
@@ -301,44 +315,111 @@ func Test_ParseAWSError(t *testing.T) {
|
||||
name: "aws-request-expired-token",
|
||||
err: TestAwsError(awsErrorExpiredToken, "The security token included in the request is expired."),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
||||
expectedPermission: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
ErrorDetails: "The security token included in the request is expired.",
|
||||
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) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
|
||||
actualStatus, permission := ParseAWSError(tt.err, "test")
|
||||
actualStatus, permission := ParseAWSError("test", tt.err)
|
||||
if tt.expectedStatusCode == codes.OK {
|
||||
require.Nil(actualStatus)
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(tt.expectedStatusCode, actualStatus.Code())
|
||||
require.Equal(tt.expectedStatusMsg, actualStatus.Message())
|
||||
require.Contains(actualStatus.Message(), tt.expectedStatusMsg)
|
||||
require.Len(actualStatus.Details(), 0)
|
||||
CheckSimilarPermission(assert, tt.expectedPermission, permission, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
@@ -6,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"
|
||||
@@ -36,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,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package values
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package values
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package plugin
|
||||
@@ -32,6 +32,6 @@ type AwsPlugin struct {
|
||||
func NewAwsPlugin() *AwsPlugin {
|
||||
return &AwsPlugin{
|
||||
HostPlugin: &host.HostPlugin{},
|
||||
StoragePlugin: &storage.StoragePlugin{},
|
||||
StoragePlugin: storage.New(),
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,14 @@ The following `attributes` are valid on an AWS host catalog resource:
|
||||
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
||||
- `region` (string): The region to configure the host catalog for. All host sets
|
||||
in this catalog will be configured for this region.
|
||||
- `role_arn` (string): The AWS Role ARN used for `AssumeRole` authentication. If
|
||||
provided, `disable_credential_rotation` must be also set to `true.
|
||||
- `role_external_id` (string): The external id configured for the `AssumeRole`
|
||||
provider.
|
||||
- `role_session_name` (string): The session name configured for the `AssumeRole`
|
||||
provider.
|
||||
- `role_tags` (object): The key-value pair tags configured for the `AssumeRole`
|
||||
provider.
|
||||
|
||||
The following `secrets` are required on an AWS host catalog resource:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
@@ -19,6 +19,9 @@ import (
|
||||
// map.
|
||||
type CatalogAttributes struct {
|
||||
*cred.CredentialAttributes
|
||||
|
||||
// DualStack is used for configuring how the aws client will resolve requests.
|
||||
DualStack bool
|
||||
}
|
||||
|
||||
func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
||||
@@ -31,6 +34,12 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dualStack, err := values.GetBoolValue(in, ConstAwsDualStack, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstAwsDualStack)] = err.Error()
|
||||
}
|
||||
delete(unknownFields, ConstAwsDualStack)
|
||||
|
||||
for s := range unknownFields {
|
||||
switch s {
|
||||
// Ignore knownFields from CredentialAttributes
|
||||
@@ -57,6 +66,7 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
||||
|
||||
return &CatalogAttributes{
|
||||
CredentialAttributes: credAttributes,
|
||||
DualStack: dualStack,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
@@ -43,6 +43,22 @@ func TestGetCatalogAttributes(t *testing.T) {
|
||||
},
|
||||
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "with dual stack",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"dual_stack": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
expected: &CatalogAttributes{
|
||||
CredentialAttributes: &credential.CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
DisableCredentialRotation: false,
|
||||
},
|
||||
DualStack: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
in: &structpb.Struct{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
@@ -10,4 +10,7 @@ const (
|
||||
// ConstInstanceNameTagKey denotes the special AWS tag key that contains an EC2
|
||||
// instance's name. This field is to be matched literally.
|
||||
ConstInstanceNameTagKey = "Name"
|
||||
|
||||
// ConstAwsDualStack is the key for the dualstack flag in the aws s3 client.
|
||||
ConstAwsDualStack = "dual_stack"
|
||||
)
|
||||
+197
-151
@@ -1,26 +1,23 @@
|
||||
package host
|
||||
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
"github.com/aws/smithy-go"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
)
|
||||
|
||||
// HostPlugin implements the HostPluginServiceServer interface for the
|
||||
@@ -42,24 +39,19 @@ var _ pb.HostPluginServiceServer = (*HostPlugin)(nil)
|
||||
func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
|
||||
catalog := req.GetCatalog()
|
||||
if catalog == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
|
||||
}
|
||||
|
||||
secrets := catalog.GetSecrets()
|
||||
if secrets == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "secrets are required")
|
||||
return nil, errors.BadRequestStatus("catalog is required")
|
||||
}
|
||||
|
||||
attrs := catalog.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "attributes are required")
|
||||
return nil, errors.BadRequestStatus("attributes are required")
|
||||
}
|
||||
|
||||
catalogAttributes, err := getCatalogAttributes(attrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
credConfig, err := cred.GetCredentialsConfig(secrets, catalogAttributes.CredentialAttributes, true)
|
||||
credConfig, err := credential.GetCredentialsConfig(catalog.GetSecrets(), catalogAttributes.CredentialAttributes, catalogAttributes.DualStack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -71,7 +63,14 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
|
||||
}
|
||||
|
||||
// Try to rotate AWS static credentials
|
||||
if credential.GetCredentialType(credState.CredentialsConfig) == credential.StaticAWS && !catalogAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
catalogState, err := newAwsCatalogPersistedState(
|
||||
@@ -80,25 +79,21 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
|
||||
}
|
||||
|
||||
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
|
||||
if !catalogAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(ctx); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
} else {
|
||||
// Simply validate if we aren't rotating.
|
||||
if err := credState.ValidateCreds(ctx); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
|
||||
}
|
||||
}
|
||||
// perform dry run to ensure we can interact with AWS as expected.
|
||||
opts := []ec2Option{}
|
||||
if catalogAttributes.DualStack {
|
||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
||||
}
|
||||
if st := dryRunValidation(ctx, catalogState, opts); st != nil {
|
||||
return nil, st.Err()
|
||||
}
|
||||
|
||||
persistedProto, err := catalogState.toProto()
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
return nil, errors.BadRequestStatusf("error converting persisted state to proto: %s", err)
|
||||
}
|
||||
|
||||
return &pb.OnCreateCatalogResponse{
|
||||
@@ -111,43 +106,105 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
// For host catalogs we only need to really validate what is
|
||||
// currently in the new copy of the host catalog data, so skip
|
||||
// fetching the new copy to keep things a little less messy.
|
||||
catalog := req.GetNewCatalog()
|
||||
if catalog == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "new catalog is nil")
|
||||
currentCatalog := req.GetCurrentCatalog()
|
||||
if currentCatalog == nil {
|
||||
return nil, errors.BadRequestStatus("current catalog is required")
|
||||
}
|
||||
newCatalog := req.GetNewCatalog()
|
||||
if newCatalog == nil {
|
||||
return nil, errors.BadRequestStatus("new catalog is required")
|
||||
}
|
||||
|
||||
var updateSecrets bool
|
||||
secrets := catalog.GetSecrets()
|
||||
if secrets != nil {
|
||||
// We will be updating secrets this run, but what exactly that
|
||||
// means will be determined later.
|
||||
updateSecrets = true
|
||||
oldAttrs := currentCatalog.GetAttributes()
|
||||
if oldAttrs == nil {
|
||||
return nil, errors.BadRequestStatus("current catalog attributes are required")
|
||||
}
|
||||
|
||||
attrs := catalog.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "new catalog missing attributes")
|
||||
oldCatalogAttributes, err := getCatalogAttributes(oldAttrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
catalogAttributes, err := getCatalogAttributes(attrs)
|
||||
newAttrs := newCatalog.GetAttributes()
|
||||
if newAttrs == nil {
|
||||
return nil, errors.BadRequestStatus("new catalog attributes are required")
|
||||
}
|
||||
newCatalogAttributes, err := getCatalogAttributes(newAttrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the persisted data.
|
||||
// NOTE: We might need to change this at a later time. I'm not too
|
||||
// sure *exactly* what scenarios we might encounter that would
|
||||
// ultimately mean that we would have to handle an empty or missing
|
||||
// state in update, but what we are ultimately assuming here
|
||||
// (implicitly through awsCatalogPersistedStateFromProto) is that
|
||||
// the state will exist and be populated. Personally I think this
|
||||
// is fine and important, but this may change in the future.
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
catalogAttributes.CredentialAttributes,
|
||||
oldCatalogAttributes.CredentialAttributes,
|
||||
oldCatalogAttributes.DualStack,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "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
|
||||
// user if they're not. Note this doesn't validate the credentials against
|
||||
// AWS - it only does logical validation on the fields.
|
||||
updatedCredentials, err := credential.GetCredentialsConfig(newCatalog.GetSecrets(), newCatalogAttributes.CredentialAttributes, newCatalogAttributes.DualStack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We will be updating credentials when one of two changes occur:
|
||||
// 1. the new catalog has secrets for static credentials, in which case we need to delete the old static credentials
|
||||
// 2. the new catalog has a different roleARN value for dynamic credentials
|
||||
if newCatalog.GetSecrets() != nil || newCatalogAttributes.RoleArn != oldCatalogAttributes.RoleArn {
|
||||
// Ensure the incoming credentials are valid for interaction with the S3
|
||||
// Bucket before replacing them.
|
||||
newCredState, err := credential.NewAwsCredentialPersistedState(
|
||||
append([]credential.AwsCredentialPersistedStateOption{
|
||||
credential.WithCredentialsConfig(updatedCredentials),
|
||||
}, p.testCredStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error setting up new credential persisted state: %s", err)
|
||||
}
|
||||
newCatalogState, err := newAwsCatalogPersistedState(
|
||||
append([]awsCatalogPersistedStateOption{
|
||||
withCredentials(newCredState),
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
}
|
||||
opts := []ec2Option{}
|
||||
if newCatalogAttributes.DualStack {
|
||||
opts = append(opts, WithDualStack(newCatalogAttributes.DualStack))
|
||||
}
|
||||
if st := dryRunValidation(ctx, newCatalogState, opts); st != nil {
|
||||
return nil, st.Err()
|
||||
}
|
||||
|
||||
// Replace the existing credential state.
|
||||
// This checks the timestamp on the last rotation time as well
|
||||
// and deletes the credentials if we are managing them
|
||||
// (ie: if we've rotated them before).
|
||||
if err := credState.ReplaceCreds(ctx, updatedCredentials); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if credential.GetCredentialType(credState.CredentialsConfig) == credential.StaticAWS {
|
||||
// This is a validate check to make sure that we aren't disabling
|
||||
// rotation for credentials currently being managed by rotation.
|
||||
// This is not allowed.
|
||||
if newCatalogAttributes.DisableCredentialRotation && newCatalog.GetSecrets() == nil {
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
return nil, errors.BadRequestStatus("cannot disable rotation for already-rotated credentials")
|
||||
}
|
||||
}
|
||||
|
||||
// If we're enabling rotation now but didn't before, or have
|
||||
// freshly replaced credentials, we can rotate here.
|
||||
if !newCatalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
||||
if err := credState.RotateCreds(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
catalogState, err := newAwsCatalogPersistedState(
|
||||
@@ -156,45 +213,21 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
|
||||
if catalogAttributes.DisableCredentialRotation && !updateSecrets {
|
||||
// This is a validate check to make sure that we aren't disabling
|
||||
// rotation for credentials currently being managed by rotation.
|
||||
// This is not allowed.
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
|
||||
}
|
||||
}
|
||||
|
||||
if updateSecrets {
|
||||
catalogSecrets, err := credential.GetCredentialsConfig(secrets, catalogAttributes.CredentialAttributes, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Replace the credentials. This checks the timestamp on the last
|
||||
// rotation time as well and deletes the credentials if we are
|
||||
// managing them (ie: if we've rotated them before).
|
||||
if err := credState.ReplaceCreds(ctx, catalogSecrets); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !catalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
||||
// If we're enabling rotation now but didn't before, or have
|
||||
// freshly replaced credentials, we can rotate here.
|
||||
if err := credState.RotateCreds(ctx); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||
}
|
||||
}
|
||||
// perform dry run to ensure we can interact with AWS as expected.
|
||||
opts := []ec2Option{}
|
||||
if newCatalogAttributes.DualStack {
|
||||
opts = append(opts, WithDualStack(newCatalogAttributes.DualStack))
|
||||
}
|
||||
if st := dryRunValidation(ctx, catalogState, opts); st != nil {
|
||||
return nil, st.Err()
|
||||
}
|
||||
|
||||
persistedProto, err := catalogState.toProto()
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
return nil, errors.BadRequestStatusf("error converting persisted state to proto: %s", err)
|
||||
}
|
||||
|
||||
return &pb.OnUpdateCatalogResponse{
|
||||
@@ -206,12 +239,12 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalogRequest) (*pb.OnDeleteCatalogResponse, error) {
|
||||
catalog := req.GetCatalog()
|
||||
if catalog == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "new catalog is nil")
|
||||
return nil, errors.BadRequestStatus("catalog is required")
|
||||
}
|
||||
|
||||
attrs := catalog.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "new catalog missing attributes")
|
||||
return nil, errors.BadRequestStatus("catalog attributes are required")
|
||||
}
|
||||
|
||||
catalogAttributes, err := getCatalogAttributes(attrs)
|
||||
@@ -228,9 +261,10 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
catalogAttributes.CredentialAttributes,
|
||||
catalogAttributes.DualStack,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
_, err = newAwsCatalogPersistedState(
|
||||
@@ -239,17 +273,17 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
// try to delete static credentials
|
||||
if cred.GetCredentialType(credState.CredentialsConfig) == cred.StaticAWS {
|
||||
if credential.GetCredentialType(credState.CredentialsConfig) == credential.StaticAWS {
|
||||
if !credState.CredsLastRotatedTime.IsZero() {
|
||||
// Delete old/existing credentials. This is done with the same
|
||||
// credentials to ensure that it has the proper permissions to do
|
||||
// it.
|
||||
if err := credState.DeleteCreds(ctx); err != nil {
|
||||
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,12 +322,12 @@ func (p *HostPlugin) NormalizeSetData(ctx context.Context, req *pb.NormalizeSetD
|
||||
func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) {
|
||||
catalog := req.GetCatalog()
|
||||
if catalog == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
|
||||
return nil, errors.BadRequestStatus("catalog is required")
|
||||
}
|
||||
|
||||
catalogAttrsRaw := catalog.GetAttributes()
|
||||
if catalogAttrsRaw == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
|
||||
return nil, errors.BadRequestStatus("catalog attributes are required")
|
||||
}
|
||||
|
||||
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
||||
@@ -304,9 +338,10 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
catalogAttributes.CredentialAttributes,
|
||||
catalogAttributes.DualStack,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
catalogState, err := newAwsCatalogPersistedState(
|
||||
@@ -315,56 +350,47 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
set := req.GetSet()
|
||||
if set == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "set is nil")
|
||||
return nil, errors.BadRequestStatus("set is required")
|
||||
}
|
||||
|
||||
if set.GetAttributes() == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "set missing attributes")
|
||||
return nil, errors.BadRequestStatus("set attributes are required")
|
||||
}
|
||||
setAttrs, err := getSetAttributes(set.GetAttributes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ec2Client, err := catalogState.EC2Client(ctx)
|
||||
describeInstanceFilters, err := buildFilters(setAttrs)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error building set filters: %s", err)
|
||||
}
|
||||
|
||||
input, err := buildDescribeInstancesInput(setAttrs, true)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
|
||||
opts := []ec2Option{}
|
||||
if catalogAttributes.DualStack {
|
||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
||||
}
|
||||
|
||||
_, err = ec2Client.DescribeInstances(ctx, input)
|
||||
if err == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found")
|
||||
if st := dryRunValidation(ctx, catalogState, opts, describeInstanceFilters...); st != nil {
|
||||
return nil, st.Err()
|
||||
}
|
||||
|
||||
var apiErr smithy.APIError
|
||||
if errors.As(err, &apiErr) && apiErr.ErrorCode() == "DryRunOperation" {
|
||||
// Success
|
||||
return &pb.OnCreateSetResponse{}, nil
|
||||
}
|
||||
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error performing dry run of DescribeInstances: %s", err)
|
||||
return &pb.OnCreateSetResponse{}, nil
|
||||
}
|
||||
|
||||
// OnUpdateSet is called when a dynamic host set is updated.
|
||||
func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) {
|
||||
catalog := req.GetCatalog()
|
||||
if catalog == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
|
||||
return nil, errors.BadRequestStatus("catalog is required")
|
||||
}
|
||||
|
||||
catalogAttrsRaw := catalog.GetAttributes()
|
||||
if catalogAttrsRaw == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
|
||||
return nil, errors.BadRequestStatus("catalog attributes are required")
|
||||
}
|
||||
|
||||
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
||||
@@ -375,9 +401,10 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
catalogAttributes.CredentialAttributes,
|
||||
catalogAttributes.DualStack,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
catalogState, err := newAwsCatalogPersistedState(
|
||||
@@ -386,46 +413,38 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "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
|
||||
// set here, just need to work off of/validate the new set config
|
||||
set := req.GetNewSet()
|
||||
if set == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "new set is nil")
|
||||
return nil, errors.BadRequestStatus("new set is required")
|
||||
}
|
||||
|
||||
if set.GetAttributes() == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "new set missing attributes")
|
||||
return nil, errors.BadRequestStatus("new set attributes are required")
|
||||
}
|
||||
setAttrs, err := getSetAttributes(set.GetAttributes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ec2Client, err := catalogState.EC2Client(ctx)
|
||||
describeInstanceFilters, err := buildFilters(setAttrs)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error building set filters: %s", err)
|
||||
}
|
||||
|
||||
input, err := buildDescribeInstancesInput(setAttrs, true)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
|
||||
opts := []ec2Option{}
|
||||
if catalogAttributes.DualStack {
|
||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
||||
}
|
||||
if st := dryRunValidation(ctx, catalogState, opts, describeInstanceFilters...); st != nil {
|
||||
return nil, st.Err()
|
||||
}
|
||||
|
||||
_, err = ec2Client.DescribeInstances(ctx, input)
|
||||
if err == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found")
|
||||
}
|
||||
|
||||
var apiErr smithy.APIError
|
||||
if errors.As(err, &apiErr) && apiErr.ErrorCode() == "DryRunOperation" {
|
||||
// Success
|
||||
return &pb.OnUpdateSetResponse{}, nil
|
||||
}
|
||||
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error performing dry run of DescribeInstances: %s", err)
|
||||
return &pb.OnUpdateSetResponse{}, nil
|
||||
}
|
||||
|
||||
// OnDeleteSet is called when a dynamic host set is deleted.
|
||||
@@ -439,12 +458,12 @@ func (p *HostPlugin) OnDeleteSet(ctx context.Context, req *pb.OnDeleteSetRequest
|
||||
func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) {
|
||||
catalog := req.GetCatalog()
|
||||
if catalog == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
|
||||
return nil, errors.BadRequestStatus("catalog is required")
|
||||
}
|
||||
|
||||
catalogAttrsRaw := catalog.GetAttributes()
|
||||
if catalogAttrsRaw == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
|
||||
return nil, errors.BadRequestStatus("catalog attributes are required")
|
||||
}
|
||||
|
||||
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
||||
@@ -455,9 +474,10 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetSecrets(),
|
||||
catalogAttributes.CredentialAttributes,
|
||||
catalogAttributes.DualStack,
|
||||
p.testCredStateOpts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
catalogState, err := newAwsCatalogPersistedState(
|
||||
@@ -466,12 +486,12 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||
}
|
||||
|
||||
sets := req.GetSets()
|
||||
if sets == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "sets is nil")
|
||||
return nil, errors.BadRequestStatus("sets are required")
|
||||
}
|
||||
|
||||
// Build all the queries in advance.
|
||||
@@ -486,11 +506,11 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
for i, set := range sets {
|
||||
// Validate Id since we use it in output
|
||||
if set.GetId() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "set missing id")
|
||||
return nil, errors.BadRequestStatus("set id is required")
|
||||
}
|
||||
|
||||
if set.GetAttributes() == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "set missing attributes")
|
||||
return nil, errors.BadRequestStatusf("set %s attributes are required", set.GetId())
|
||||
}
|
||||
setAttrs, err := getSetAttributes(set.GetAttributes())
|
||||
if err != nil {
|
||||
@@ -499,7 +519,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
|
||||
input, err := buildDescribeInstancesInput(setAttrs, false)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "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(),
|
||||
@@ -507,9 +527,13 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
}
|
||||
}
|
||||
|
||||
ec2Client, err := catalogState.EC2Client(ctx)
|
||||
opts := []ec2Option{}
|
||||
if catalogAttributes.DualStack {
|
||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
||||
}
|
||||
ec2Client, err := catalogState.EC2Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||
return nil, errors.BadRequestStatusf("error getting EC2 client: %s", err)
|
||||
}
|
||||
|
||||
// Run all queries now and assemble output.
|
||||
@@ -517,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, status.Errorf(codes.InvalidArgument, "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
|
||||
@@ -528,7 +552,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
||||
for _, instance := range reservation.Instances {
|
||||
host, err := awsInstanceToHost(instance)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "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)
|
||||
@@ -626,7 +650,7 @@ func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.Descri
|
||||
// populated.
|
||||
func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, error) {
|
||||
if aws.ToString(instance.InstanceId) == "" {
|
||||
return nil, errors.New("response integrity error: missing instance id")
|
||||
return nil, fmt.Errorf("response integrity error: missing instance id")
|
||||
}
|
||||
|
||||
result := new(pb.ListHostsResponseHost)
|
||||
@@ -674,6 +698,28 @@ func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, erro
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// dryRunValidation performs an AWS DescribeInstances call to verify the state's
|
||||
// credentials, the host listing functionality as well as the filters, if any
|
||||
// are passed in. This function can therefore be used for both host catalog and
|
||||
// host set validation.
|
||||
func dryRunValidation(ctx context.Context, state *awsCatalogPersistedState, ec2Opts []ec2Option, filters ...types.Filter) *status.Status {
|
||||
if state == nil {
|
||||
return status.New(codes.InvalidArgument, "persisted state is required")
|
||||
}
|
||||
|
||||
ec2Client, err := state.EC2Client(ctx, ec2Opts...)
|
||||
if err != nil {
|
||||
return status.New(codes.InvalidArgument, fmt.Sprintf("error getting EC2 client: %s", err))
|
||||
}
|
||||
|
||||
_, err = ec2Client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{Filters: filters})
|
||||
if err != nil {
|
||||
return status.New(codes.FailedPrecondition, fmt.Sprintf("aws describe instances failed: %s", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// appendDistinct will append the elements to the slice
|
||||
// if an element is not nil, empty, and does not exist in slice.
|
||||
func appendDistinct(slice []string, elems ...*string) []string {
|
||||
|
||||
+990
-242
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
@@ -7,12 +7,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
smithyEndpoints "github.com/aws/smithy-go/endpoints"
|
||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
@@ -22,17 +20,6 @@ type EC2API interface {
|
||||
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
|
||||
}
|
||||
|
||||
var customClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
},
|
||||
}
|
||||
|
||||
type awsCatalogPersistedState struct {
|
||||
*credential.AwsCredentialPersistedState
|
||||
|
||||
@@ -86,7 +73,11 @@ func (s *awsCatalogPersistedState) toProto() (*pb.HostCatalogPersisted, error) {
|
||||
|
||||
// EC2Client returns a configured EC2 client based on the session
|
||||
// information stored in the state.
|
||||
func (s *awsCatalogPersistedState) EC2Client(ctx context.Context) (EC2API, error) {
|
||||
func (s *awsCatalogPersistedState) EC2Client(ctx context.Context, opt ...ec2Option) (EC2API, error) {
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing options when fetching EC2 client: %w", err)
|
||||
}
|
||||
awsCfg, err := s.GenerateCredentialChain(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting AWS configuration when fetching EC2 client: %w", err)
|
||||
@@ -94,8 +85,58 @@ func (s *awsCatalogPersistedState) EC2Client(ctx context.Context) (EC2API, error
|
||||
if awsCfg == nil {
|
||||
return nil, fmt.Errorf("nil aws configuration")
|
||||
}
|
||||
var ec2Opts []func(*ec2.Options)
|
||||
if opts.withDualStack {
|
||||
ec2Opts = append(ec2Opts, ec2.WithEndpointResolverV2(&endpointResolver{
|
||||
dualStack: opts.withDualStack,
|
||||
}))
|
||||
}
|
||||
if s.testEC2APIFunc != nil {
|
||||
return s.testEC2APIFunc(*awsCfg)
|
||||
}
|
||||
return ec2.NewFromConfig(*awsCfg), nil
|
||||
return ec2.NewFromConfig(*awsCfg, ec2Opts...), nil
|
||||
}
|
||||
|
||||
// getOpts iterates the inbound ec2Options and returns a struct
|
||||
func getOpts(opt ...ec2Option) (ec2Options, error) {
|
||||
opts := getDefaultEc2Options()
|
||||
for _, o := range opt {
|
||||
if o == nil {
|
||||
continue
|
||||
}
|
||||
if err := o(&opts); err != nil {
|
||||
return ec2Options{}, err
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// ec2Option - how ec2Options are passed as arguments
|
||||
type ec2Option func(*ec2Options) error
|
||||
|
||||
// options = how options are represented
|
||||
type ec2Options struct {
|
||||
withDualStack bool
|
||||
}
|
||||
|
||||
func getDefaultEc2Options() ec2Options {
|
||||
return ec2Options{}
|
||||
}
|
||||
|
||||
// WithDualStack sets the dual stack resolver
|
||||
func WithDualStack(with bool) ec2Option {
|
||||
return func(o *ec2Options) error {
|
||||
o.withDualStack = with
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type endpointResolver struct {
|
||||
dualStack bool
|
||||
}
|
||||
|
||||
func (e *endpointResolver) ResolveEndpoint(ctx context.Context, params ec2.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) {
|
||||
params.UseDualStack = aws.Bool(e.dualStack)
|
||||
resolver, err = ec2.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
|
||||
return
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
@@ -19,6 +19,9 @@ type StorageAttributes struct {
|
||||
|
||||
// EndpointUrl is used for configuring how the aws client will resolve requests.
|
||||
EndpointUrl string
|
||||
|
||||
// DualStack is used for configuring how the aws client will resolve requests.
|
||||
DualStack bool
|
||||
}
|
||||
|
||||
func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
||||
@@ -37,6 +40,12 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
||||
}
|
||||
delete(unknownFields, ConstAwsEndpointUrl)
|
||||
|
||||
dualStack, err := values.GetBoolValue(in, ConstAwsDualStack, false)
|
||||
if err != nil {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstAwsDualStack)] = err.Error()
|
||||
}
|
||||
delete(unknownFields, ConstAwsDualStack)
|
||||
|
||||
for s := range unknownFields {
|
||||
switch s {
|
||||
// Ignore knownFields from CredentialAttributes
|
||||
@@ -64,5 +73,6 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
||||
return &StorageAttributes{
|
||||
CredentialAttributes: credAttributes,
|
||||
EndpointUrl: endpointUrl,
|
||||
DualStack: dualStack,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
@@ -41,6 +41,21 @@ func TestGetStorageAttributes(t *testing.T) {
|
||||
},
|
||||
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "good w/ dualstack",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"region": structpb.NewStringValue("us-west-2"),
|
||||
"dual_stack": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
expected: &StorageAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
Region: "us-west-2",
|
||||
},
|
||||
DualStack: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good w/ endpoint",
|
||||
in: &structpb.Struct{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
@@ -7,6 +7,9 @@ const (
|
||||
// ConstAwsEndpointUrl is the key for the endpoint url in the aws s3 client.
|
||||
ConstAwsEndpointUrl = "endpoint_url"
|
||||
|
||||
// ConstAwsDualStack is the key for the dualstack flag in the aws s3 client.
|
||||
ConstAwsDualStack = "dual_stack"
|
||||
|
||||
// defaultStreamChunkSize is the recommened chunk size for sending data through a stream
|
||||
defaultStreamChunkSize = 65536 // 64 KiB
|
||||
)
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
@@ -37,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
|
||||
}
|
||||
@@ -47,12 +54,12 @@ func setStatePermission(req any, permission *pb.Permission) *pb.StorageBucketCre
|
||||
// credentials, and connectivity error types.
|
||||
//
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
|
||||
func parseS3Error(err error, msg string, req any) *status.Status {
|
||||
func parseS3Error(op string, err error, req any) *status.Status {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
st, p := errors.ParseAWSError(err, msg)
|
||||
st, p := errors.ParseAWSError(op, err)
|
||||
if p != nil {
|
||||
state := setStatePermission(req, p)
|
||||
if st, err = st.WithDetails(state); err != nil {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
@@ -31,7 +34,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("NoSuchBucket", "GetObject", "no such bucket"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws s3 error: test",
|
||||
expectedStatusMsg: "aws service s3: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -47,7 +50,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("NoSuchKey", "GetObject", "no such key"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws s3 error: test",
|
||||
expectedStatusMsg: "aws service s3: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -63,7 +66,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
err: errors.TestAwsHttpResponseError(404, "not found"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
// cannot embed services within mock http errors, therefore unknown service
|
||||
expectedStatusMsg: "aws service unknown: resource not found error: test",
|
||||
expectedStatusMsg: "aws service unknown: resource not found: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -78,7 +81,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("InvalidObjectState", "GetObject", "invalid object state"),
|
||||
expectedStatusCode: codes.NotFound,
|
||||
expectedStatusMsg: "aws s3 error: test",
|
||||
expectedStatusMsg: "aws service s3: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -93,7 +96,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.PutObjectRequest{},
|
||||
err: TestAwsS3Error(s3ErrorBadDigest, "PutObject", "checksum mismatch"),
|
||||
expectedStatusCode: codes.Aborted,
|
||||
expectedStatusMsg: "aws s3 error: test",
|
||||
expectedStatusMsg: "aws service s3: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Write: &pb.Permission{
|
||||
@@ -108,7 +111,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("Throttling", "action", "throttling exception"),
|
||||
expectedStatusCode: codes.Unavailable,
|
||||
expectedStatusMsg: "aws service s3: throttling error: test",
|
||||
expectedStatusMsg: "aws service s3: throttling: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -123,7 +126,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("RequestTimeout", "action", "request timeout"),
|
||||
expectedStatusCode: codes.DeadlineExceeded,
|
||||
expectedStatusMsg: "aws s3 error: test",
|
||||
expectedStatusMsg: "aws service s3: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -138,7 +141,7 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
req: &pb.GetObjectRequest{},
|
||||
err: TestAwsS3Error("AccessDenied", "action", "access denied"),
|
||||
expectedStatusCode: codes.PermissionDenied,
|
||||
expectedStatusMsg: "aws service s3: invalid credentials error: test",
|
||||
expectedStatusMsg: "aws service s3: invalid credentials: test:",
|
||||
expectedDetails: &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Read: &pb.Permission{
|
||||
@@ -154,14 +157,14 @@ func Test_ParseS3Error(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
|
||||
actualStatus := parseS3Error(tt.err, "test", tt.req)
|
||||
actualStatus := parseS3Error("test", tt.err, tt.req)
|
||||
if tt.expectedStatusCode == codes.OK {
|
||||
require.Nil(actualStatus)
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(tt.expectedStatusCode, actualStatus.Code())
|
||||
require.Equal(tt.expectedStatusMsg, actualStatus.Message())
|
||||
require.Contains(actualStatus.Message(), tt.expectedStatusMsg)
|
||||
require.Len(actualStatus.Details(), 1)
|
||||
|
||||
foundDetail := false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
stderr "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
@@ -24,14 +26,24 @@ import (
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// Ensure that we are implementing StoragePluginServiceServer
|
||||
var _ pb.StoragePluginServiceServer = (*StoragePlugin)(nil)
|
||||
|
||||
// clientCache caches previously initialized S3API clients created with a RoleARN attribute.
|
||||
type clientCache struct {
|
||||
// cache is a map of cached s3 clients. The key of the map
|
||||
// is the public id of the storage bucket
|
||||
cache map[string]S3API
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// StoragePlugin implements the StoragePluginServiceServer interface for the
|
||||
// AWS storage service plugin.
|
||||
type StoragePlugin struct {
|
||||
@@ -42,6 +54,137 @@ type StoragePlugin struct {
|
||||
|
||||
// testStorageStateOpts are passed in to the stored state to control test behavior
|
||||
testStorageStateOpts []awsStoragePersistedStateOption
|
||||
|
||||
clients *clientCache
|
||||
}
|
||||
|
||||
// New creates a new StoragePlugin
|
||||
func New() *StoragePlugin {
|
||||
c := &clientCache{
|
||||
cache: make(map[string]S3API),
|
||||
}
|
||||
return &StoragePlugin{
|
||||
clients: c,
|
||||
}
|
||||
}
|
||||
|
||||
// getClient returns an S3API client for the given storage bucket id.
|
||||
func (p *StoragePlugin) getClient(ctx context.Context,
|
||||
storageBucketId string,
|
||||
storageState *awsStoragePersistedState,
|
||||
opt ...s3Option) (S3API, error) {
|
||||
if storageBucketId == "" {
|
||||
// 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.BadRequestStatusf("error creating S3 client: %s", err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing options in cache: %w", err)
|
||||
}
|
||||
|
||||
if opts.withCacheRefresh {
|
||||
// Forced cache refresh
|
||||
p.clients.Lock()
|
||||
defer p.clients.Unlock()
|
||||
client, err := storageState.s3Client(ctx, opt...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error creating S3 client: %s", err)
|
||||
}
|
||||
p.clients.cache[storageBucketId] = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
p.clients.RLock()
|
||||
client, ok := p.clients.cache[storageBucketId]
|
||||
p.clients.RUnlock()
|
||||
|
||||
if !ok || client.Credentials().Expired() {
|
||||
// We got a cache miss or the credentials has expired, time to refresh
|
||||
p.clients.Lock()
|
||||
defer p.clients.Unlock()
|
||||
|
||||
// Check cache again in case another caller updated it since we got the lock
|
||||
client, ok = p.clients.cache[storageBucketId]
|
||||
if ok && !client.Credentials().Expired() {
|
||||
// Got a cache hit with valid credentials, return cached client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Create new client and cache it
|
||||
var err error
|
||||
client, err = storageState.s3Client(ctx, opt...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error creating S3 client: %s", err)
|
||||
}
|
||||
p.clients.cache[storageBucketId] = client
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
type s3Caller func(client S3API) (any, error)
|
||||
|
||||
// call gets the s3client and passes it to the s3caller function, if a permission error is returned
|
||||
// it forces a cache refresh and tries again.
|
||||
func (p *StoragePlugin) call(
|
||||
ctx context.Context,
|
||||
fn s3Caller,
|
||||
storageBucketId string,
|
||||
storageState *awsStoragePersistedState,
|
||||
opts ...s3Option) (any, error) {
|
||||
|
||||
s3Client, err := p.getClient(ctx, storageBucketId, storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error getting S3 client: %s", err)
|
||||
}
|
||||
firstAttempt := true
|
||||
for {
|
||||
resp, err := fn(s3Client)
|
||||
if err != nil {
|
||||
st, _ := errors.ParseAWSError("", err)
|
||||
if st.Code() == codes.PermissionDenied && firstAttempt {
|
||||
// We got a permission error, this is the first time so refresh cache and try again
|
||||
firstAttempt = false
|
||||
opts = append(opts, WithCacheRefresh(true))
|
||||
s3Client, err = p.getClient(ctx, storageBucketId, storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error refreshing S3 client: %s", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeStorageBucketData is called by boundary before calling OnCreate or OnUpdate
|
||||
//
|
||||
// aws storage supports a total of 8 attributes, 6 as part of credential.CredentialAttributes
|
||||
// and 2 more from storage.StorageAttributes. of these, only endpoint_url is normalized.
|
||||
func (sp *StoragePlugin) NormalizeStorageBucketData(ctx context.Context, req *pb.NormalizeStorageBucketDataRequest) (*pb.NormalizeStorageBucketDataResponse, error) {
|
||||
sa, err := getStorageAttributes(req.GetAttributes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fields := req.GetAttributes().GetFields()
|
||||
if _, ok := fields[ConstAwsEndpointUrl]; ok {
|
||||
if sa.EndpointUrl, err = parseutil.NormalizeAddr(sa.EndpointUrl); err != nil {
|
||||
return nil, status.New(codes.InvalidArgument, fmt.Sprintf("failed to normalize provided %s: %s", ConstAwsEndpointUrl, err.Error())).Err()
|
||||
}
|
||||
fields[ConstAwsEndpointUrl] = structpb.NewStringValue(sa.EndpointUrl)
|
||||
}
|
||||
|
||||
return &pb.NormalizeStorageBucketDataResponse{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: fields,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OnCreateStorageBucket is called when a storage bucket is created.
|
||||
@@ -64,7 +207,7 @@ func (p *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCre
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -76,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
|
||||
@@ -92,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.
|
||||
@@ -147,15 +290,16 @@ func (p *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpd
|
||||
credState, err := cred.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetData(),
|
||||
oldStorageAttributes.CredentialAttributes,
|
||||
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
|
||||
// user if they're not. Note this doesn't validate the credentials against
|
||||
// AWS - it only does logical validation on the fields.
|
||||
updatedCredentials, err := cred.GetCredentialsConfig(newBucket.GetSecrets(), newStorageAttributes.CredentialAttributes, false)
|
||||
updatedCredentials, err := cred.GetCredentialsConfig(newBucket.GetSecrets(), newStorageAttributes.CredentialAttributes, newStorageAttributes.DualStack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -172,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{
|
||||
@@ -180,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()
|
||||
@@ -220,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.
|
||||
@@ -264,9 +408,10 @@ func (p *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDel
|
||||
credState, err := cred.AwsCredentialPersistedStateFromProto(
|
||||
req.GetPersisted().GetData(),
|
||||
storageAttributes.CredentialAttributes,
|
||||
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(
|
||||
@@ -275,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
|
||||
@@ -318,7 +463,7 @@ func (p *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectReques
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -330,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(
|
||||
@@ -339,28 +484,32 @@ 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{}
|
||||
if storageAttributes.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
||||
}
|
||||
s3Client, err := storageState.S3Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error getting S3 client: %s", err)
|
||||
if storageAttributes.DualStack {
|
||||
opts = append(opts, WithDualStack(storageAttributes.DualStack))
|
||||
}
|
||||
|
||||
objectKey := path.Join(bucket.GetBucketPrefix(), req.GetKey())
|
||||
resp, err := s3Client.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, parseS3Error(err, "failed to head object", req).Err()
|
||||
headCall := func(s3Client S3API) (any, error) {
|
||||
return s3Client.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
}
|
||||
headResp, err := p.call(ctx, headCall, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, parseS3Error("head object", err, req).Err()
|
||||
}
|
||||
resp := headResp.(*s3.HeadObjectOutput)
|
||||
|
||||
return &pb.HeadObjectResponse{
|
||||
ContentLength: resp.ContentLength,
|
||||
ContentLength: aws.ToInt64(resp.ContentLength),
|
||||
LastModified: timestamppb.New(*resp.LastModified),
|
||||
}, nil
|
||||
}
|
||||
@@ -386,7 +535,7 @@ func (p *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Validat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -398,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(
|
||||
@@ -407,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.
|
||||
@@ -444,7 +593,7 @@ func (p *StoragePlugin) GetObject(req *pb.GetObjectRequest, stream pb.StoragePlu
|
||||
return err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -456,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(
|
||||
@@ -465,26 +614,29 @@ 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{}
|
||||
if storageAttributes.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
||||
}
|
||||
s3Client, err := storageState.S3Client(stream.Context(), opts...)
|
||||
if err != nil {
|
||||
return errors.BadRequestStatus("error getting S3 client: %s", err)
|
||||
if storageAttributes.DualStack {
|
||||
opts = append(opts, WithDualStack(storageAttributes.DualStack))
|
||||
}
|
||||
|
||||
objectKey := path.Join(bucket.GetBucketPrefix(), req.GetKey())
|
||||
resp, err := s3Client.GetObject(stream.Context(), &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
if err != nil {
|
||||
return parseS3Error(err, "failed to get object", req).Err()
|
||||
getCall := func(s3Client S3API) (any, error) {
|
||||
return s3Client.GetObject(stream.Context(), &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
}
|
||||
getResp, err := p.call(stream.Context(), getCall, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return parseS3Error("get object", err, req).Err()
|
||||
}
|
||||
resp := getResp.(*s3.GetObjectOutput)
|
||||
|
||||
defer resp.Body.Close()
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
@@ -496,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 {
|
||||
@@ -555,7 +707,7 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -567,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(
|
||||
@@ -576,16 +728,15 @@ 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{}
|
||||
if storageAttributes.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
||||
}
|
||||
s3Client, err := storageState.S3Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error getting S3 client: %s", err)
|
||||
if storageAttributes.DualStack {
|
||||
opts = append(opts, WithDualStack(storageAttributes.DualStack))
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
@@ -598,18 +749,23 @@ func (p *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest)
|
||||
if err != nil {
|
||||
return nil, errors.UnknownStatus("failed to rewind file pointer")
|
||||
}
|
||||
|
||||
objectKey := path.Join(bucket.GetBucketPrefix(), req.GetKey())
|
||||
resp, err := s3Client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(objectKey),
|
||||
Body: file,
|
||||
ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
|
||||
ChecksumSHA256: aws.String(checksum),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, parseS3Error(err, "failed to put object", req).Err()
|
||||
|
||||
putCall := func(s3Client S3API) (any, error) {
|
||||
return s3Client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(objectKey),
|
||||
Body: file,
|
||||
ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
|
||||
ChecksumSHA256: aws.String(checksum),
|
||||
})
|
||||
}
|
||||
putResp, err := p.call(ctx, putCall, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, parseS3Error("put object", err, req).Err()
|
||||
}
|
||||
resp := putResp.(*s3.PutObjectOutput)
|
||||
|
||||
if resp.ChecksumSHA256 == nil {
|
||||
return nil, errors.UnknownStatus("missing checksum response from aws")
|
||||
}
|
||||
@@ -650,7 +806,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, false)
|
||||
credConfig, err := cred.GetCredentialsConfig(bucket.GetSecrets(), storageAttributes.CredentialAttributes, storageAttributes.DualStack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -662,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(
|
||||
@@ -671,17 +827,13 @@ 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{}
|
||||
if storageAttributes.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(storageAttributes.EndpointUrl))
|
||||
}
|
||||
client, err := storageState.S3Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatus("error getting S3 client: %s", err)
|
||||
}
|
||||
|
||||
prefix := path.Join(bucket.GetBucketPrefix(), req.GetKeyPrefix())
|
||||
if strings.HasSuffix(req.GetKeyPrefix(), "/") {
|
||||
@@ -691,12 +843,15 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
|
||||
}
|
||||
|
||||
if !req.Recursive {
|
||||
_, err := client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(prefix),
|
||||
})
|
||||
deleteCall := func(s3Client S3API) (any, error) {
|
||||
return s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Key: aws.String(prefix),
|
||||
})
|
||||
}
|
||||
_, err := p.call(ctx, deleteCall, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, parseS3Error(err, "failed to delete object", req).Err()
|
||||
return nil, parseS3Error("delete object", err, req).Err()
|
||||
}
|
||||
return &pb.DeleteObjectsResponse{
|
||||
ObjectsDeleted: uint32(1),
|
||||
@@ -704,21 +859,36 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
|
||||
}
|
||||
|
||||
const maxkeys = 1000
|
||||
|
||||
objects := []types.ObjectIdentifier{}
|
||||
client, err := p.getClient(ctx, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error getting S3 client: %s", err)
|
||||
}
|
||||
var conToken *string
|
||||
truncated := true
|
||||
truncated, firstAttempt := true, true
|
||||
for truncated {
|
||||
res, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucket.GetBucketName()),
|
||||
Prefix: aws.String(prefix),
|
||||
MaxKeys: maxkeys,
|
||||
MaxKeys: aws.Int32(maxkeys),
|
||||
ContinuationToken: conToken,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, parseS3Error(err, "failed to list objects", req).Err()
|
||||
st, _ := errors.ParseAWSError("", err)
|
||||
if st.Code() == codes.PermissionDenied && firstAttempt {
|
||||
// This is first attempt and we get a permission error, refresh cache and try again
|
||||
firstAttempt = false
|
||||
opts = append(opts, WithCacheRefresh(true))
|
||||
client, err = p.getClient(ctx, bucket.GetId(), storageState, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequestStatusf("error refreshing S3 client: %s", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, parseS3Error("list objects", err, req).Err()
|
||||
}
|
||||
truncated = res.IsTruncated
|
||||
truncated = aws.ToBool(res.IsTruncated)
|
||||
conToken = res.NextContinuationToken
|
||||
for _, o := range res.Contents {
|
||||
objects = append(objects, types.ObjectIdentifier{
|
||||
@@ -738,7 +908,7 @@ func (p *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjects
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, parseS3Error(err, "failed to delete objects", req).Err()
|
||||
return nil, parseS3Error("delete objects", err, req).Err()
|
||||
}
|
||||
deleted += len(res.Deleted)
|
||||
}
|
||||
@@ -748,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
|
||||
@@ -769,8 +1124,11 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
if attrs.EndpointUrl != "" {
|
||||
opts = append(opts, WithEndpoint(attrs.EndpointUrl))
|
||||
}
|
||||
if attrs.DualStack {
|
||||
opts = append(opts, WithDualStack(attrs.DualStack))
|
||||
}
|
||||
|
||||
client, err := state.S3Client(ctx, opts...)
|
||||
client, err := state.s3Client(ctx, opts...)
|
||||
if err != nil {
|
||||
return status.New(codes.InvalidArgument, fmt.Sprintf("error getting S3 client: %s", err))
|
||||
}
|
||||
@@ -786,7 +1144,7 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
Body: bytes.NewReader([]byte("hashicorp boundary aws plugin access test")),
|
||||
}); err != nil {
|
||||
var st *status.Status
|
||||
st, permissions.Write = errors.ParseAWSError(err, "failed to put object")
|
||||
st, permissions.Write = errors.ParseAWSError("put object", err)
|
||||
errs = multierror.Append(errs, st.Err())
|
||||
} else {
|
||||
permissions.Write = &pb.Permission{State: pb.StateType_STATE_TYPE_OK, CheckedAt: timestamppb.Now()}
|
||||
@@ -797,7 +1155,7 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
Key: aws.String(objectKey),
|
||||
}); err != nil {
|
||||
var st *status.Status
|
||||
st, permissions.Read = errors.ParseAWSError(err, "failed to get object")
|
||||
st, permissions.Read = errors.ParseAWSError("get object", err)
|
||||
errs = multierror.Append(errs, st.Err())
|
||||
} else {
|
||||
permissions.Read = &pb.Permission{State: pb.StateType_STATE_TYPE_OK, CheckedAt: timestamppb.Now()}
|
||||
@@ -808,7 +1166,7 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
Key: aws.String(objectKey),
|
||||
}); err != nil {
|
||||
var st *status.Status
|
||||
st, permissions.Read = errors.ParseAWSError(err, "failed to head object")
|
||||
st, permissions.Read = errors.ParseAWSError("head object", err)
|
||||
errs = multierror.Append(errs, st.Err())
|
||||
}
|
||||
|
||||
@@ -817,10 +1175,11 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
Prefix: aws.String(objectKey),
|
||||
}); err != nil {
|
||||
var st *status.Status
|
||||
st, permissions.Read = errors.ParseAWSError(err, "failed to list object")
|
||||
st, permissions.Read = errors.ParseAWSError("list object", err)
|
||||
errs = multierror.Append(errs, st.Err())
|
||||
} else if res == nil || len(res.Contents) != 1 || *res.Contents[0].Key != objectKey {
|
||||
return status.New(codes.Aborted, fmt.Sprintf("list response did not contain the expected key: %+v", res))
|
||||
permissions.Read = &pb.Permission{State: pb.StateType_STATE_TYPE_UNKNOWN, CheckedAt: timestamppb.Now()}
|
||||
errs = multierror.Append(errs, status.New(codes.Aborted, fmt.Sprintf("list response did not contain the expected key: %+v", res)).Err())
|
||||
}
|
||||
|
||||
if _, err := client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
@@ -828,7 +1187,7 @@ func dryRunValidation(ctx context.Context, state *awsStoragePersistedState, attr
|
||||
Key: aws.String(objectKey),
|
||||
}); err != nil {
|
||||
var st *status.Status
|
||||
st, permissions.Delete = errors.ParseAWSError(err, "failed to delete object")
|
||||
st, permissions.Delete = errors.ParseAWSError("delete object", err)
|
||||
errs = multierror.Append(errs, st.Err())
|
||||
} else {
|
||||
permissions.Delete = &pb.Permission{State: pb.StateType_STATE_TYPE_OK, CheckedAt: timestamppb.Now()}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
@@ -7,10 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
@@ -27,17 +24,16 @@ type S3API interface {
|
||||
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
|
||||
DeleteObjects(ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error)
|
||||
ListObjectsV2(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error)
|
||||
Credentials() aws.Credentials
|
||||
}
|
||||
|
||||
var customClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
},
|
||||
type s3Client struct {
|
||||
*s3.Client
|
||||
creds aws.Credentials
|
||||
}
|
||||
|
||||
func (c *s3Client) Credentials() aws.Credentials {
|
||||
return c.creds
|
||||
}
|
||||
|
||||
type awsStoragePersistedState struct {
|
||||
@@ -91,9 +87,9 @@ func (s *awsStoragePersistedState) toProto() (*storagebuckets.StorageBucketPersi
|
||||
return &storagebuckets.StorageBucketPersisted{Data: data}, nil
|
||||
}
|
||||
|
||||
// S3Client returns a configured S3 client based on the session
|
||||
// s3Client returns a configured S3 client based on the session
|
||||
// information stored in the state.
|
||||
func (s *awsStoragePersistedState) S3Client(ctx context.Context, opt ...s3Option) (S3API, error) {
|
||||
func (s *awsStoragePersistedState) s3Client(ctx context.Context, opt ...s3Option) (S3API, error) {
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing options when fetching S3 client: %w", err)
|
||||
@@ -106,15 +102,25 @@ func (s *awsStoragePersistedState) S3Client(ctx context.Context, opt ...s3Option
|
||||
return nil, fmt.Errorf("nil aws configuration")
|
||||
}
|
||||
var s3Opts []func(*s3.Options)
|
||||
if opts.withEndpoint != "" {
|
||||
if opts.withEndpoint != "" || opts.withDualStack {
|
||||
s3Opts = append(s3Opts, s3.WithEndpointResolverV2(&endpointResolver{
|
||||
endpoint: opts.withEndpoint,
|
||||
endpoint: opts.withEndpoint,
|
||||
dualStack: opts.withDualStack,
|
||||
}))
|
||||
}
|
||||
if s.testS3APIFunc != nil {
|
||||
return s.testS3APIFunc(*awsCfg)
|
||||
}
|
||||
return s3.NewFromConfig(*awsCfg, s3Opts...), nil
|
||||
|
||||
// Retrieve the credentials provider from the client
|
||||
credsProvider := awsCfg.Credentials
|
||||
creds, err := credsProvider.Retrieve(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve credentials: %v\n", err)
|
||||
}
|
||||
|
||||
c := s3.NewFromConfig(*awsCfg, s3Opts...)
|
||||
return &s3Client{Client: c, creds: creds}, nil
|
||||
}
|
||||
|
||||
// getOpts iterates the inbound s3Options and returns a struct
|
||||
@@ -136,13 +142,23 @@ type s3Option func(*s3Options) error
|
||||
|
||||
// options = how options are represented
|
||||
type s3Options struct {
|
||||
withEndpoint string
|
||||
withEndpoint string
|
||||
withDualStack bool
|
||||
withCacheRefresh bool
|
||||
}
|
||||
|
||||
func getDefaultS3Options() s3Options {
|
||||
return s3Options{}
|
||||
}
|
||||
|
||||
// WithCacheRefresh controls if the cache should be forced to refresh
|
||||
func WithCacheRefresh(refresh bool) s3Option {
|
||||
return func(o *s3Options) error {
|
||||
o.withCacheRefresh = refresh
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEndpoint contains the endpoint to use
|
||||
func WithEndpoint(with string) s3Option {
|
||||
return func(o *s3Options) error {
|
||||
@@ -151,18 +167,27 @@ func WithEndpoint(with string) s3Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithDualStack sets the dual stack resolver
|
||||
func WithDualStack(with bool) s3Option {
|
||||
return func(o *s3Options) error {
|
||||
o.withDualStack = with
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type endpointResolver struct {
|
||||
endpoint string
|
||||
endpoint string
|
||||
dualStack bool
|
||||
}
|
||||
|
||||
func (e *endpointResolver) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) (resolver smithyEndpoints.Endpoint, err error) {
|
||||
var uri *url.URL
|
||||
params.UseDualStack = aws.Bool(e.dualStack)
|
||||
resolver, err = s3.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if e.endpoint == "" {
|
||||
// TODO: not sure if we should set an error or ignore the missing endpoint value and continue with the default resolver
|
||||
return
|
||||
}
|
||||
uri, err = url.Parse(e.endpoint)
|
||||
@@ -170,7 +195,6 @@ func (e *endpointResolver) ResolveEndpoint(ctx context.Context, params s3.Endpoi
|
||||
return
|
||||
}
|
||||
if uri == nil {
|
||||
// TODO: not sure if we should set an error or ignore the missing uri value and continue with the default resolver
|
||||
return
|
||||
}
|
||||
resolver = smithyEndpoints.Endpoint{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
@@ -64,7 +64,7 @@ func TestAwsStoragePersistedStateS3Client(t *testing.T) {
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
clientRaw, err := state.S3Client(context.Background(), tc.s3Opts...)
|
||||
clientRaw, err := state.s3Client(context.Background(), tc.s3Opts...)
|
||||
require.NoError(err)
|
||||
require.NotNil(clientRaw)
|
||||
client, ok := clientRaw.(*testMockS3)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package storage
|
||||
|
||||
+16
-10
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
@@ -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 {
|
||||
|
||||
+320
-8
@@ -1,10 +1,11 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
@@ -17,12 +18,16 @@ import (
|
||||
"github.com/hashicorp/boundary-plugin-aws/plugin/service/storage"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
// NOTE: this test does not vaildate against dynamic credentials because
|
||||
// this credential type can only be tested on an EC2 instance.
|
||||
func TestStoragePlugin(t *testing.T) {
|
||||
region := os.Getenv("AWS_REGION")
|
||||
if region == "" {
|
||||
@@ -75,6 +80,18 @@ func TestStoragePlugin(t *testing.T) {
|
||||
iamSecretAccessKeyMissingPutObject, err := tf.GetOutputString("iam_secret_access_key_missing_put_obj")
|
||||
require.NoError(err)
|
||||
|
||||
iamAccessKeyMissingDeleteObject, err := tf.GetOutputString("iam_access_key_missing_delete_obj")
|
||||
require.NoError(err)
|
||||
|
||||
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
|
||||
@@ -121,22 +138,75 @@ func TestStoragePlugin(t *testing.T) {
|
||||
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
|
||||
|
||||
// ********************
|
||||
// * Object Methods: PutObject, GetObject, HeadObject
|
||||
// * Object Methods: PutObject, GetObject, HeadObject, DeleteObjects
|
||||
// ********************
|
||||
//
|
||||
// Reassign the keyid and secret first.
|
||||
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
|
||||
// ********************
|
||||
//
|
||||
var expectedSBCState *pb.StorageBucketCredentialState
|
||||
|
||||
// Validate error is returned for missing get object permission
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingGetObject, iamSecretAccessKeyMissingGetObject)
|
||||
expectedSBCState = &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Write: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_OK,
|
||||
},
|
||||
Read: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
},
|
||||
Delete: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_OK,
|
||||
},
|
||||
},
|
||||
}
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingGetObject, iamSecretAccessKeyMissingGetObject, expectedSBCState)
|
||||
|
||||
// Validate error is returned for missing put object permission
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingPutObject, iamSecretAccessKeyMissingPutObject)
|
||||
expectedSBCState = &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Write: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
},
|
||||
Read: &pb.Permission{
|
||||
// Since we cannot write an object to S3,
|
||||
// we cannot test against the ability to read from S3.
|
||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
||||
},
|
||||
Delete: &pb.Permission{
|
||||
// We can still try deleting an object that does
|
||||
// not exist in the S3 bucket to validate access.
|
||||
State: pb.StateType_STATE_TYPE_OK,
|
||||
},
|
||||
},
|
||||
}
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingPutObject, iamSecretAccessKeyMissingPutObject, expectedSBCState)
|
||||
|
||||
// Validate error is returned for missing delete object permission
|
||||
expectedSBCState = &pb.StorageBucketCredentialState{
|
||||
State: &pb.Permissions{
|
||||
Write: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_OK,
|
||||
},
|
||||
Read: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_OK,
|
||||
},
|
||||
Delete: &pb.Permission{
|
||||
State: pb.StateType_STATE_TYPE_ERROR,
|
||||
},
|
||||
},
|
||||
}
|
||||
testPluginValidatePermissions(ctx, t, p, bucketName, region, iamAccessKeyMissingDeleteObject, iamSecretAccessKeyMissingDeleteObject, expectedSBCState)
|
||||
}
|
||||
|
||||
func testPluginOnCreateStorageBucket(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
||||
@@ -289,10 +359,12 @@ func testPluginOnDeleteStorageBucket(ctx context.Context, t *testing.T, p *stora
|
||||
}
|
||||
}
|
||||
|
||||
func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
|
||||
func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, expectedState *pb.StorageBucketCredentialState) {
|
||||
t.Helper()
|
||||
t.Logf("testing ValidatePermissions")
|
||||
require := require.New(t)
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
|
||||
require.NotNil(expectedState)
|
||||
|
||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||
credential.ConstRegion: region,
|
||||
@@ -315,6 +387,51 @@ func testPluginValidatePermissions(ctx context.Context, t *testing.T, p *storage
|
||||
|
||||
_, err = p.ValidatePermissions(ctx, req)
|
||||
require.Error(err)
|
||||
|
||||
var actualSBCState *pb.StorageBucketCredentialState
|
||||
if st, ok := status.FromError(err); ok {
|
||||
for _, detail := range st.Details() {
|
||||
if statusDetail, ok := detail.(*pb.StorageBucketCredentialState); ok {
|
||||
actualSBCState = statusDetail
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
require.NotNil(actualSBCState)
|
||||
require.NotNil(actualSBCState.GetState())
|
||||
|
||||
expectedReadState := expectedState.GetState().GetRead()
|
||||
if expectedReadState != nil {
|
||||
actualReadState := actualSBCState.GetState().GetRead()
|
||||
require.NotNil(actualReadState)
|
||||
assert.Equal(expectedReadState.GetState(), actualReadState.GetState())
|
||||
assert.NotEmpty(actualReadState.GetCheckedAt())
|
||||
if expectedReadState.GetState() == pb.StateType_STATE_TYPE_ERROR {
|
||||
assert.NotEmpty(actualReadState.GetErrorDetails())
|
||||
}
|
||||
}
|
||||
|
||||
expectedWriteState := expectedState.GetState().GetWrite()
|
||||
if expectedWriteState != nil {
|
||||
actualWriteState := actualSBCState.GetState().GetWrite()
|
||||
require.NotNil(actualWriteState)
|
||||
assert.Equal(expectedWriteState.GetState(), actualWriteState.GetState())
|
||||
assert.NotEmpty(actualWriteState.GetCheckedAt())
|
||||
if expectedWriteState.GetState() == pb.StateType_STATE_TYPE_ERROR {
|
||||
assert.NotEmpty(actualWriteState.GetErrorDetails())
|
||||
}
|
||||
}
|
||||
|
||||
expectedDeleteState := expectedState.GetState().GetDelete()
|
||||
if expectedDeleteState != nil {
|
||||
actualDeleteState := actualSBCState.GetState().GetDelete()
|
||||
require.NotNil(actualDeleteState)
|
||||
assert.Equal(expectedDeleteState.GetState(), actualDeleteState.GetState())
|
||||
assert.NotEmpty(actualDeleteState.GetCheckedAt())
|
||||
if expectedDeleteState.GetState() == pb.StateType_STATE_TYPE_ERROR {
|
||||
assert.NotEmpty(actualDeleteState.GetErrorDetails())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string) {
|
||||
@@ -369,7 +486,7 @@ func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.Stora
|
||||
|
||||
// Need to create a GRPC server to test GetObject
|
||||
t.Logf("testing GetObject")
|
||||
lis, err := net.Listen("tcp", "localhost:2030")
|
||||
lis, err := net.Listen("tcp", "[::1]:2030")
|
||||
require.NoError(err)
|
||||
grpcServer := grpc.NewServer()
|
||||
pb.RegisterStoragePluginServiceServer(grpcServer, &storage.StoragePlugin{})
|
||||
@@ -377,7 +494,7 @@ func testPluginObjectMethods(ctx context.Context, t *testing.T, p *storage.Stora
|
||||
grpcServer.Serve(lis)
|
||||
}()
|
||||
|
||||
conn, err := grpc.Dial("localhost:2030", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
conn, err := grpc.Dial("[::1]:2030", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.NoError(err)
|
||||
defer conn.Close()
|
||||
client := pb.NewStoragePluginServiceClient(conn)
|
||||
@@ -409,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)
|
||||
})
|
||||
})
|
||||
}
|
||||
Vendored
+22
-15
@@ -1,24 +1,20 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# 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
|
||||
{
|
||||
|
||||
Vendored
+2
-2
@@ -1,11 +1,11 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 3.0"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+49
@@ -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"
|
||||
},
|
||||
]
|
||||
}
|
||||
Vendored
+12
-26
@@ -1,28 +1,6 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# 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,
|
||||
]
|
||||
}
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
resource "random_id" "foo" {
|
||||
|
||||
Vendored
+104
-17
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
resource "aws_iam_role" "valid" {
|
||||
@@ -32,10 +32,20 @@ resource "aws_iam_policy" "valid" {
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes"
|
||||
"s3:DeleteObject",
|
||||
"s3:GetObjectAttributes",
|
||||
"s3:ListBucket",
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -79,7 +89,9 @@ resource "aws_iam_policy" "missing_put_obj" {
|
||||
{
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes"
|
||||
"s3:GetObjectAttributes",
|
||||
"s3:ListBucket",
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
@@ -125,7 +137,7 @@ resource "aws_iam_policy" "missing_get_obj" {
|
||||
{
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObjectAttributes"
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
@@ -141,18 +153,67 @@ resource "aws_iam_role_policy_attachment" "missing_get_obj" {
|
||||
policy_arn = aws_iam_policy.missing_get_obj.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "missing_delete_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-delete-obj"
|
||||
tags = local.tags
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Sid = ""
|
||||
Principal = {
|
||||
AWS: data.aws_caller_identity.current.user_id
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "missing_delete_obj" {
|
||||
name = "${random_id.prefix.dec}-missing-delete-obj"
|
||||
tags = local.tags
|
||||
|
||||
policy = <<EOT
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes",
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "missing_delete_obj" {
|
||||
role = aws_iam_role.missing_delete_obj.name
|
||||
policy_arn = aws_iam_policy.missing_delete_obj.arn
|
||||
}
|
||||
|
||||
# Static Credential Testing
|
||||
resource "random_id" "user_name" {
|
||||
count = var.iam_user_count
|
||||
prefix = random_id.prefix.dec
|
||||
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" "test" {
|
||||
count = var.iam_user_count
|
||||
name = random_id.user_name[count.index].dec
|
||||
force_destroy = true
|
||||
tags = local.tags
|
||||
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" # 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" {
|
||||
@@ -195,17 +256,42 @@ resource "aws_iam_user_policy_attachment" "test_credentials" {
|
||||
policy_arn = aws_iam_policy.credentials[count.index].arn
|
||||
}
|
||||
|
||||
# Edge case for missing DeleteObject permission
|
||||
resource "random_id" "missing_delete_obj" {
|
||||
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "aws_iam_user" "missing_delete_obj" {
|
||||
name = random_id.missing_delete_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
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_access_key" "missing_delete_obj" {
|
||||
user = aws_iam_user.missing_delete_obj.name
|
||||
}
|
||||
|
||||
resource "aws_iam_user_policy_attachment" "missing_delete_obj" {
|
||||
user = aws_iam_user.missing_delete_obj.name
|
||||
policy_arn = aws_iam_policy.missing_delete_obj.arn
|
||||
}
|
||||
|
||||
# Edge case for missing GetObject permission
|
||||
|
||||
resource "random_id" "missing_get_obj" {
|
||||
prefix = random_id.prefix.dec
|
||||
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
|
||||
tags = local.tags
|
||||
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" {
|
||||
@@ -220,14 +306,15 @@ resource "aws_iam_user_policy_attachment" "missing_get_obj" {
|
||||
# Edge case for missing PutObject permission
|
||||
|
||||
resource "random_id" "missing_put_obj" {
|
||||
prefix = random_id.prefix.dec
|
||||
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
|
||||
tags = local.tags
|
||||
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" {
|
||||
|
||||
Vendored
+2
-2
@@ -1,11 +1,11 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 3.0"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+25
-6
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
output "bucket_name" {
|
||||
@@ -6,15 +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_put_get_policy_arn" {
|
||||
value = aws_iam_role.missing_get_obj.arn
|
||||
output "missing_get_obj_policy_arn" {
|
||||
value = aws_iam_role.missing_get_obj.arn
|
||||
}
|
||||
|
||||
output "missing_delete_obj_policy_arn" {
|
||||
value = aws_iam_role.missing_delete_obj.arn
|
||||
}
|
||||
|
||||
output "iam_user_names" {
|
||||
@@ -53,4 +57,19 @@ output "iam_access_key_missing_get_obj" {
|
||||
output "iam_secret_access_key_missing_get_obj" {
|
||||
value = aws_iam_access_key.missing_get_obj.secret
|
||||
sensitive = true
|
||||
}
|
||||
}
|
||||
|
||||
output "iam_access_key_missing_delete_obj" {
|
||||
value = aws_iam_access_key.missing_delete_obj.id
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Vendored
+19
-3
@@ -1,9 +1,25 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
resource "aws_s3_bucket" "test" {
|
||||
bucket = random_id.prefix.dec
|
||||
force_destroy = true
|
||||
acl = "private"
|
||||
tags = local.tags
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Vendored
+47
-5
@@ -1,10 +1,10 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2021, 2026
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
resource "random_id" "prefix" {
|
||||
prefix = "storage-e2e-test"
|
||||
prefix = "aws-plugin-e2e-test"
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
@@ -13,8 +13,50 @@ variable "iam_user_count" {
|
||||
}
|
||||
|
||||
locals {
|
||||
hashicorp_email = split(":", data.aws_caller_identity.current.user_id)[1]
|
||||
tags = {
|
||||
user_id = split(":", data.aws_caller_identity.current.user_id)[1]
|
||||
repo = "boundary-plugin-aws"
|
||||
User = split(":", data.aws_caller_identity.current.user_id)[1]
|
||||
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/" = ""
|
||||
}
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2021, 2026
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
|
||||
Reference in new issue
Block a user