Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b22c64aa64 | ||
|
|
009d2cd570 |
No files matched your search
@@ -1,11 +0,0 @@
|
|||||||
## 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.
|
|
||||||
@@ -13,9 +13,9 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
|
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
|
||||||
with:
|
with:
|
||||||
go-version-file: 'go.mod'
|
go-version-file: 'go.mod'
|
||||||
cache: true
|
cache: true
|
||||||
|
|||||||
@@ -4,12 +4,3 @@ plan.tfplan
|
|||||||
*.log
|
*.log
|
||||||
*.out
|
*.out
|
||||||
*.html
|
*.html
|
||||||
|
|
||||||
# IntelliJ IDEA project files
|
|
||||||
.idea
|
|
||||||
*.iprg
|
|
||||||
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
.env
|
|
||||||
.envrc
|
|
||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
# These owners will be the default owners for everything in
|
# These owners will be the default owners for everything in
|
||||||
# the repo, unless a later match takes precedence.
|
# the repo, unless a later match takes precedence.
|
||||||
|
|
||||||
* @hashicorp/boundary
|
@hashicorp/boundary
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright IBM Corp. 2021, 2026
|
Copyright (c) 2021 HashiCorp, Inc.
|
||||||
|
|
||||||
Mozilla Public License Version 2.0
|
Mozilla Public License Version 2.0
|
||||||
==================================
|
==================================
|
||||||
|
|||||||
@@ -5,32 +5,15 @@ Boundary](https://www.boundaryproject.io/).
|
|||||||
|
|
||||||
## Credential Rotation
|
## Credential Rotation
|
||||||
|
|
||||||
This is the following priority for the credential chain:
|
Although credentials are stored encrypted within Boundary, by default this
|
||||||
static, assume role, environment variables.
|
|
||||||
|
|
||||||
### Static Credentials
|
|
||||||
|
|
||||||
Although static credentials are stored encrypted within Boundary, by default this
|
|
||||||
plugin will attempt to rotate credentials when they are supplied through the
|
plugin will attempt to rotate credentials when they are supplied through the
|
||||||
`secrets` object. The given credentials will be used to create a new credential,
|
`secrets` object. The given credentials will be used to create a new credential,
|
||||||
and then the given credential will be revoked. In this way, after rotation,
|
and then the given credential will be revoked. In this way, after rotation,
|
||||||
only Boundary knows the client secret in use by this plugin. More information
|
only Boundary knows the client secret in use by this plugin.
|
||||||
about AWS static credentials can be found [here](https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html).
|
|
||||||
|
|
||||||
Credential rotation can be turned off by setting the
|
Credential rotation can be turned off by setting the
|
||||||
`disable_credential_rotation` attribute to `true`.
|
`disable_credential_rotation` attribute to `true`.
|
||||||
|
|
||||||
### Assume Role Credentials
|
|
||||||
|
|
||||||
This plugin will attempt to assume a role when a `role_arn` is supplied through the
|
|
||||||
`attributes` object. More information about assume an AWS role can be found [here](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html). This feature only works when the plugin is running on a self managed Boundary [worker](https://developer.hashicorp.com/boundary/tutorials/hcp-administration/hcp-manage-workers).
|
|
||||||
|
|
||||||
### Environment Credentials
|
|
||||||
|
|
||||||
This plugin will attempt to retrieve credentials from environment variables. More
|
|
||||||
information about environment variables for AWS credentials can be found [here](https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html). This feature only works when the plugin is running on
|
|
||||||
a self managed Boundary [worker](https://developer.hashicorp.com/boundary/tutorials/hcp-administration/hcp-manage-workers).
|
|
||||||
|
|
||||||
## Dynamic Hosts
|
## Dynamic Hosts
|
||||||
|
|
||||||
This plugin supports dynamically sourcing hosts from Amazon EC2.
|
This plugin supports dynamically sourcing hosts from Amazon EC2.
|
||||||
@@ -48,7 +31,7 @@ The plugin fetches hosts through the
|
|||||||
[DescribeInstances](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html)
|
[DescribeInstances](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html)
|
||||||
call.
|
call.
|
||||||
|
|
||||||
[Getting Started](plugin/service/host/README.md)
|
[Getting Started](https://github.com/hashicorp/boundary-plugin-aws/plugin/service/host/README.md)
|
||||||
|
|
||||||
## Storage Bucket
|
## Storage Bucket
|
||||||
|
|
||||||
@@ -60,7 +43,7 @@ these storage bucket resources can in turn be associated to targets within
|
|||||||
Boundary.
|
Boundary.
|
||||||
|
|
||||||
At creation, update or deletion of a storage bucket of this type, configuration of the
|
At creation, update or deletion of a storage bucket of this type, configuration of the
|
||||||
plugin is performed via the attribute/secret values passed to the create, update, or
|
plugin is performed via the attribute/secret values passed to the create, update, or
|
||||||
delete calls actions. The values passed in to the plugin here are the attributes set
|
delete calls actions. The values passed in to the plugin here are the attributes set
|
||||||
on on a storage bucket in boundary.
|
on on a storage bucket in boundary.
|
||||||
|
|
||||||
@@ -76,4 +59,4 @@ The plugin fetches metadata about the files through the
|
|||||||
[HeadObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html)
|
[HeadObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html)
|
||||||
call.
|
call.
|
||||||
|
|
||||||
[Getting Started](plugin/service/storage/README.md)
|
[Getting Started](https://github.com/hashicorp/boundary-plugin-aws/plugin/service/storage/README.md)
|
||||||
@@ -1,69 +1,66 @@
|
|||||||
module github.com/hashicorp/boundary-plugin-aws
|
module github.com/hashicorp/boundary-plugin-aws
|
||||||
|
|
||||||
go 1.25.7
|
go 1.20
|
||||||
|
|
||||||
|
replace github.com/hashicorp/boundary/sdk => github.com/hashicorp/boundary/sdk v0.0.35-0.20230705165757-c7e98a6f1782
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.7
|
github.com/aws/aws-sdk-go v1.44.80
|
||||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0
|
github.com/aws/aws-sdk-go-v2 v1.17.7
|
||||||
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6
|
github.com/aws/aws-sdk-go-v2/config v1.18.19
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0
|
github.com/aws/aws-sdk-go-v2/credentials v1.13.18
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1
|
||||||
github.com/aws/smithy-go v1.25.1
|
github.com/google/go-cmp v0.5.9
|
||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/hashicorp/boundary/sdk v0.0.35
|
||||||
github.com/hashicorp/boundary/sdk v0.0.59
|
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6
|
||||||
github.com/hashicorp/go-multierror v1.1.1
|
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/mitchellh/mapstructure v1.5.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.8.2
|
||||||
google.golang.org/grpc v1.79.3
|
google.golang.org/grpc v1.53.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.28.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 // 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/s3shared v1.14.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect
|
github.com/aws/smithy-go v1.13.5 // 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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.14.1 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/eventlogger v0.2.11 // indirect
|
github.com/hashicorp/eventlogger v0.1.2-0.20230428153751-cca445805f24 // indirect
|
||||||
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20231025104552-802587e608f0 // indirect
|
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20230428153751-cca445805f24 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||||
github.com/hashicorp/go-kms-wrapping/v2 v2.0.20 // indirect
|
github.com/hashicorp/go-kms-wrapping/v2 v2.0.9 // indirect
|
||||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
|
||||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
github.com/hashicorp/go-version v1.5.0 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/pointerstructure v1.2.1 // indirect
|
github.com/mitchellh/pointerstructure v1.2.1 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
github.com/zclconf/go-cty v1.10.0 // indirect
|
||||||
github.com/zclconf/go-cty v1.15.0 // indirect
|
golang.org/x/crypto v0.8.0 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/net v0.9.0 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/sys v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/text v0.9.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488 // 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
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
@@ -1,171 +1,150 @@
|
|||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
|
||||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||||
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 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/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||||
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 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE=
|
github.com/aws/aws-sdk-go v1.44.80 h1:jEXGecSgPdvM5KnyDsSgFhZSm7WwaTp4h544Im4SfhI=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
github.com/aws/aws-sdk-go v1.44.80/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
github.com/aws/aws-sdk-go-v2 v1.17.7 h1:CLSjnhJSTSogvqUGhIC6LqFKATMRexcxLZ0i/Nzk9Eg=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
|
github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||||
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.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
|
||||||
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.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
|
github.com/aws/aws-sdk-go-v2/config v1.18.19 h1:AqFK6zFNtq4i1EYu+eC7lcKHYnZagMn6SW171la0bGw=
|
||||||
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.18.19/go.mod h1:XvTmGMY8d52ougvakOv1RpiTLPz9dlG/OQHsKU/cMmY=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0=
|
github.com/aws/aws-sdk-go-v2/credentials v1.13.18 h1:EQMdtHwz0ILTW1hoP+EwuWhwCG1hD6l3+RWFQABET4c=
|
||||||
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.13.18/go.mod h1:vnwlwjIe+3XJPBYKu1et30ZPABG3VaXJYr8ryohpIyM=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 h1:gt57MN3liKiyGopcqgNzJb2+d9MJaKT/q1OksHNXVE4=
|
||||||
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.13.1/go.mod h1:lfUx8puBRdM5lVVMQlwt2v+ofiG/X6Ms+dy0UkG/kXw=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 h1:sJLYcS+eZn5EeNINGHSCRAwUJMFVqklwkH36Vbyai7M=
|
||||||
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.1.31/go.mod h1:QT0BqUvX1Bh2ABdTGnjqEjvjzrCfIniM9Sc8zn9Yndo=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 h1:1mnRASEKnkqsntcxHaysxwgVoUUp5dkiB+l3llKnqyg=
|
||||||
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/endpoints/v2 v2.4.25/go.mod h1:zBHOPwhBc3FlQjQJE/D3IfPWiWaQmT06Vq9aNukDo0k=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 h1:p5luUImdIqywn6JpQsW3tq5GNOxKmOnEpybzPx+d1lk=
|
||||||
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/ini v1.3.32/go.mod h1:XGhIBZDEgfqmFIugclZ6FU7v75nHhBDtzuB4xB/tEi4=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 h1:DWYZIsyqagnWL00f8M/SOr9fN063OEQWn9LLTbdYXsk=
|
||||||
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/v4a v1.0.23/go.mod h1:uIiFgURZbACBEQJfqTZPb/jxO7R+9LeoHUFudtIdeQI=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
|
||||||
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/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 h1:CeuSeq/8FnYpPtnuIeLQEEvDv9zUjneuYi8EghMBdwQ=
|
||||||
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/service/internal/checksum v1.1.26/go.mod h1:2UqAAwMUXKeRkAHIlDJqvMVgOWkUi/AUXPk/YIe+Dg4=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.8 h1:1/bT9kDdLQzfZ1e6J6hpW+SfNDd6xrV8F3M2CuGyUz8=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 h1:5LHn8JQ0qvjD9L9JhMtylnkcw7j05GDZqM9Oin6hpr0=
|
||||||
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/service/internal/presigned-url v1.9.25/go.mod h1:/95IA+0lMnzW6XzqYJRpjjsAbKEORVeO0anQqjd2CNU=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 h1:e2ooMhpYGhDnBfSvIyusvAwX7KexuZaHbQY2Dyei7VU=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0/go.mod h1:bh2E0CXKZsQN+faiKVqC40vfNMAWheoULBCnEgO9K+8=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0 h1:fTLR6dLDTGChAjecRPlVrKeznT0rVdzR4yn9Z68MTGk=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1 h1:PJH4I+qYjPXclKRbVCW47iYUvtXEh1u6YmDhn5J8VQE=
|
||||||
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/s3 v1.31.1/go.mod h1:ncltU6n4Nof5uJttDtcNQ537uNuwYqsZZQcpkd2/GUQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6 h1:EWehQXACWr+6hzfZPwZChlfoVhiUCfLHE0Xh3kAfzWQ=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 h1:5V7DWLBd7wTELVz5bPpwzYy/sikk0gsgZfj40X+l5OI=
|
||||||
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/sso v1.12.6/go.mod h1:Y1VOmit/Fn6Tz1uFAeCO6Q7M2fmfXSCLeL5INVYsLuY=
|
||||||
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/ssooidc v1.14.6 h1:B8cauxOH1W1v7rd8RdI/MWnoR4Ze0wIHWrb90qczxj4=
|
||||||
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/ssooidc v1.14.6/go.mod h1:Lh/bc9XUf8CfOY6Jp5aIkQtN+j1mc+nExc+KXj9jx2s=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 h1:bWNgNdRko2x6gqa0blfATqAZKZokPIeM1vfmQt2pnvM=
|
||||||
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/sts v1.18.7/go.mod h1:JuTnSoeePXmMVe9G8NcjjwgOKEfZ4cOjMuT2IBT/2eI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.8 h1:tIN8MFT1z5STK5kTdOT1TCfMN/bn5fSEnlKsTL8qBOU=
|
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.8/go.mod h1:VKS56txtNWjKI8FqD/hliL0BcshyF4ZaLBa1rm2Y+5s=
|
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||||
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 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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/hashicorp/boundary/sdk v0.0.59 h1:XNniO0rwgihLdF1+4qDHKN3gdta3AU1JteZResCJ8Oc=
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/hashicorp/boundary/sdk v0.0.59/go.mod h1:VYwVGAEaHTzihRXpzKZL6EhWBhll59Iul3PGLVL63ao=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/boundary/sdk v0.0.35-0.20230705165757-c7e98a6f1782 h1:MuBxNJCn7At92jVlc17GMOghSkTy+33/DOBh/JvaVxU=
|
||||||
|
github.com/hashicorp/boundary/sdk v0.0.35-0.20230705165757-c7e98a6f1782/go.mod h1:jn9j5mM8v2pOk8aLeJNIszdm7WBf4gRSP68iy8Iu5Q0=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
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 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/eventlogger v0.2.11 h1:lkK8ARM/DrCeL5deH2yVS6p40337dESi+77p9pIfLqo=
|
github.com/hashicorp/eventlogger v0.1.2-0.20230428153751-cca445805f24 h1:vtCJgcxwDMFrpUgVMfjtwXY1YqvHbj8pNA7GsXYEoLg=
|
||||||
github.com/hashicorp/eventlogger v0.2.11/go.mod h1:Rmc3MEopz7jUnLokZVTWAsXaEP5rqd20ObGS+pcau3U=
|
github.com/hashicorp/eventlogger v0.1.2-0.20230428153751-cca445805f24/go.mod h1://CHt6/j+Q2lc0NlUB5af4aS2M0c0aVBg9/JfcpAyhM=
|
||||||
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.20230428153751-cca445805f24 h1:mopKPTxSaVr24uSysyRwywTDs85L5b05qqZvEkWa0qc=
|
||||||
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20231025104552-802587e608f0/go.mod h1:tMywUTIvdB/FXhwm6HMTt61C8/eODY6gitCHhXtyojg=
|
github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20230428153751-cca445805f24/go.mod h1:EQPLoX6CONA9BSYUovTQBHfPGE91g7wOxv03sO29FzY=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
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-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||||
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.8 h1:/GIUjn9GkFXMk/8/irRdbdtmx8CcyeyWdVy/E5LvzyA=
|
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.8/go.mod h1:JDc9UOD4EVRDIwPVethJcT5Ibi/Nas6eQDPtA60iwP0=
|
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.4 h1:8z+Zi0uLYXONkxwWb/D7O5t1IUucB6Ft3uvSOa950bM=
|
||||||
github.com/hashicorp/go-kms-wrapping/v2 v2.0.20 h1:afreZ1WJd0WI7v4NsMZ1aL7V/T59sxPuKmQDgGUja20=
|
github.com/hashicorp/go-kms-wrapping/v2 v2.0.9 h1:JpCvi97NMA+saNqO8ovQcGoRbBq6P5ZZlJqvOsW5ick=
|
||||||
github.com/hashicorp/go-kms-wrapping/v2 v2.0.20/go.mod h1:NeK2Ul15t1zutp/dZzt28XQrGZHosbxE/QLNNfaWObM=
|
github.com/hashicorp/go-kms-wrapping/v2 v2.0.9/go.mod h1:NtMaPhqSlfQ72XWDD2g80o8HI8RKkowIB8/WZHMyPY4=
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
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-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
|
github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU=
|
||||||
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
|
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 h1:W9WN8p6moV1fjKLkeqEgkAMu5rauy9QeYDAmIaPuuiA=
|
||||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.2 h1:KlDrZWx/wYpnUcFPB0s9uhPjyNZoeIfhrV2MN/Ly76s=
|
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg=
|
||||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.2/go.mod h1:6+rVulOPNCQbL3Xv2iLCqM0JmU2WO2wRzP1C6hBKeB8=
|
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
|
||||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng=
|
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 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.9 h1:YZ01/aiARPneDUryzjiPGvr3pmjCSC+1WBZKI1P7I7k=
|
||||||
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.13 h1:TayxZ5drfMP0G6T++WvnLESGLOWeHtdWDWWTAi2e3Qk=
|
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.6 h1:rOmnqOSUBn5opme1Kh9OXO8qWd0KztO+f02vvCwCc/w=
|
||||||
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/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs=
|
||||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.10 h1:2iDz+t0JLl1W0tJhvmhsh/UBgT1JgC8Qxz8HxYMWXQo=
|
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.4 h1:IU2iGcvthrJ53rPbQU6B4+iVs/cLPO89ti5dd2XEj3k=
|
||||||
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 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 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.2 h1:phcbL8urUzF/kxA/Oj6awENaRwfWsjP59GW7u2qlDyY=
|
||||||
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.3 h1:xbrxd0U9XQW8qL1BAz2XrAjAF/P2vcqUTAues9c24B8=
|
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
||||||
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.3/go.mod h1:LWq2Sy8UoKKuK4lFuCNWSjJj57MhNNf2zzBWMtkAIX4=
|
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 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E=
|
||||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
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.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI=
|
github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM=
|
||||||
github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c=
|
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||||
github.com/hashicorp/yamux v0.1.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 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 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 h1:E87tDTVS5W65euzixn7clSzK66puSt1H4I5SC0EmHH4=
|
||||||
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f/go.mod h1:3J2qVK16Lq8V+wfiL2lPeDZ7UWMxk5LemerHa1p6N00=
|
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||||
|
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 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
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.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||||
|
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
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/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/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
@@ -174,65 +153,121 @@ github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8oh
|
|||||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
||||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
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/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 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 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
go.uber.org/goleak v1.0.0 h1:qsup4IcBdlmsnGfqyLl4Ntn3C2XCCuKAE7DwHpScyUo=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
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-20200202094626-16171245cfb2/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.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||||
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
|
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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/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-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||||
|
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-20230303212802-e74f57abe488 h1:QQF+HdiI4iocoxUjjpLgvTYDHKm99C/VtTBFnfiCJos=
|
||||||
|
google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA=
|
||||||
|
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||||
|
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||||
|
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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||||
|
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package credential
|
package credential
|
||||||
@@ -6,9 +6,10 @@ package credential
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
|
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
||||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,48 +20,33 @@ type CredentialAttributes struct {
|
|||||||
|
|
||||||
// DisableCredentialRotation disables the rotation of aws secrets associated with the plugin
|
// DisableCredentialRotation disables the rotation of aws secrets associated with the plugin
|
||||||
DisableCredentialRotation bool
|
DisableCredentialRotation bool
|
||||||
|
|
||||||
// RoleArn is the role arn associated with the aws credentials
|
|
||||||
RoleArn string
|
|
||||||
|
|
||||||
// RoleExternalId is the external id associated with the aws credentials
|
|
||||||
RoleExternalId string
|
|
||||||
|
|
||||||
// RoleSessionName is the session name associated with the aws credentials
|
|
||||||
RoleSessionName string
|
|
||||||
|
|
||||||
// RoleTags is the tags associated with the aws credentials
|
|
||||||
RoleTags map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCredentialsConfig parses values out of a protobuf struct secrets and returns a
|
// GetCredentialsConfig parses values out of a protobuf struct input and returns a
|
||||||
// CredentialsConfig used for configuring an AWS session. An status error is returned
|
// CredentialsConfig used for configuring an AWS session. An error is returned if
|
||||||
// with an InvalidArgument code if any unrecognized fields are found in the protobuf
|
// any of the following fields are missing from the protobuf struct input or have
|
||||||
// struct input.
|
// invalid value types: access_key_id, secret_access_key. An error is returned if
|
||||||
func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, dualStack bool) (*awsutil.CredentialsConfig, error) {
|
// any unrecognized fields are found in the protobuf struct input.
|
||||||
// initialize secrets if it is nil
|
func GetCredentialsConfig(in *structpb.Struct, region string) (*awsutil.CredentialsConfig, error) {
|
||||||
// secrets can be nil because static credentials are optional
|
unknownFields := values.StructFields(in)
|
||||||
if secrets == nil {
|
|
||||||
secrets = &structpb.Struct{
|
|
||||||
Fields: make(map[string]*structpb.Value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unknownFields := values.StructFields(secrets)
|
|
||||||
badFields := make(map[string]string)
|
badFields := make(map[string]string)
|
||||||
|
|
||||||
accessKey, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
|
accessKey, err := values.GetStringValue(in, ConstAccessKeyId, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = err.Error()
|
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = err.Error()
|
||||||
}
|
}
|
||||||
delete(unknownFields, ConstAccessKeyId)
|
delete(unknownFields, ConstAccessKeyId)
|
||||||
|
|
||||||
secretKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, false)
|
secretKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error()
|
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error()
|
||||||
}
|
}
|
||||||
delete(unknownFields, ConstSecretAccessKey)
|
delete(unknownFields, ConstSecretAccessKey)
|
||||||
|
|
||||||
|
if region == "" {
|
||||||
|
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
// the creds_last_rotated_time field will be found in the input struct for
|
// the creds_last_rotated_time field will be found in the input struct for
|
||||||
// persisted secrets, this value is not needed for creating the CredentialsConfig
|
// persisted secrets, this value is not needed for creating the CredentialsConfig
|
||||||
delete(unknownFields, ConstCredsLastRotatedTime)
|
delete(unknownFields, ConstCredsLastRotatedTime)
|
||||||
@@ -68,78 +54,21 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
|
|||||||
badFields[fmt.Sprintf("secrets.%s", s)] = "unrecognized field"
|
badFields[fmt.Sprintf("secrets.%s", s)] = "unrecognized field"
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := []awsutil.Option{}
|
|
||||||
// logic for parsing the credential type
|
|
||||||
// supported types:
|
|
||||||
// - user static credential
|
|
||||||
// - ec2 assume role dynamic credential
|
|
||||||
// - enviornment variables credential
|
|
||||||
switch {
|
|
||||||
// static credentials and dynamic credentials cannot be used together
|
|
||||||
case accessKey != "" && secretKey != "" && attrs.RoleArn != "":
|
|
||||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = "conflicts with role_arn value"
|
|
||||||
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = "conflicts with role_arn value"
|
|
||||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleArn)] = "conflicts with access_key_id and secret_access_key values"
|
|
||||||
// static credential is missing it's secret_access_key
|
|
||||||
case accessKey != "" && secretKey == "":
|
|
||||||
badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = "missing required value"
|
|
||||||
// static credential is missing it's access_key_id
|
|
||||||
case accessKey == "" && secretKey != "":
|
|
||||||
badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = "missing required value"
|
|
||||||
// dynamic credentials and credential rotation is not supported
|
|
||||||
case len(attrs.RoleArn) > 0 && !attrs.DisableCredentialRotation:
|
|
||||||
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = "disable_credential_rotation attribute is required when providing a role_arn"
|
|
||||||
// add static credentials
|
|
||||||
case accessKey != "" && secretKey != "":
|
|
||||||
opts = append(opts,
|
|
||||||
awsutil.WithAccessKey(accessKey),
|
|
||||||
awsutil.WithSecretKey(secretKey),
|
|
||||||
)
|
|
||||||
// add dynamic credentials
|
|
||||||
case attrs.RoleArn != "":
|
|
||||||
opts = append(opts, awsutil.WithRoleArn(attrs.RoleArn))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(badFields) > 0 {
|
if len(badFields) > 0 {
|
||||||
return nil, errors.InvalidArgumentError("Error in the secrets provided", badFields)
|
return nil, errors.InvalidArgumentError("Error in the secrets provided", badFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
if attrs.Region != "" {
|
return awsutil.NewCredentialsConfig(
|
||||||
opts = append(opts, awsutil.WithRegion(attrs.Region))
|
awsutil.WithAccessKey(accessKey),
|
||||||
}
|
awsutil.WithSecretKey(secretKey),
|
||||||
if attrs.RoleExternalId != "" {
|
awsutil.WithRegion(region),
|
||||||
opts = append(opts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
|
)
|
||||||
}
|
|
||||||
if attrs.RoleSessionName != "" {
|
|
||||||
opts = append(opts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
|
|
||||||
}
|
|
||||||
if len(attrs.RoleTags) != 0 {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCredentialAttributes parses values out of a protobuf struct input and returns a
|
// GetCredentialAttributes parses values out of a protobuf struct input and returns a
|
||||||
// CredentialAttributes used for configuring an AWS session. An status error is returned
|
// CredentialAttributes used for configuring an AWS session. An error is returned if
|
||||||
// with an InvalidArgument code if any of the following fields are missing from the protobuf
|
// any of the following fields are missing from the protobuf struct input or have
|
||||||
// struct input or have invalid value types: region, disableCredentialRotation
|
// invalid value types: region, disableCredentialRotation
|
||||||
func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error) {
|
func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error) {
|
||||||
badFields := make(map[string]string)
|
badFields := make(map[string]string)
|
||||||
|
|
||||||
@@ -148,31 +77,17 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
|
|||||||
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
|
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if region != "" {
|
||||||
|
if _, found := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), region); !found {
|
||||||
|
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = fmt.Sprintf("not a valid region: %s", region)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disableCredentialRotation, err := values.GetBoolValue(in, ConstDisableCredentialRotation, false)
|
disableCredentialRotation, err := values.GetBoolValue(in, ConstDisableCredentialRotation, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = err.Error()
|
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
roleArn, err := values.GetStringValue(in, ConstRoleArn, false)
|
|
||||||
if err != nil {
|
|
||||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleArn)] = err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
roleExternalId, err := values.GetStringValue(in, ConstRoleExternalId, false)
|
|
||||||
if err != nil {
|
|
||||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleExternalId)] = err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
roleSessionName, err := values.GetStringValue(in, ConstRoleSessionName, false)
|
|
||||||
if err != nil {
|
|
||||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleSessionName)] = err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
roleTags, err := values.GetMapStringString(in, ConstRoleTags, false)
|
|
||||||
if err != nil {
|
|
||||||
badFields[fmt.Sprintf("attributes.%s", ConstRoleTags)] = err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(badFields) > 0 {
|
if len(badFields) > 0 {
|
||||||
return nil, errors.InvalidArgumentError("Error in the attributes provided", badFields)
|
return nil, errors.InvalidArgumentError("Error in the attributes provided", badFields)
|
||||||
}
|
}
|
||||||
@@ -180,9 +95,5 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
|
|||||||
return &CredentialAttributes{
|
return &CredentialAttributes{
|
||||||
Region: region,
|
Region: region,
|
||||||
DisableCredentialRotation: disableCredentialRotation,
|
DisableCredentialRotation: disableCredentialRotation,
|
||||||
RoleArn: roleArn,
|
|
||||||
RoleExternalId: roleExternalId,
|
|
||||||
RoleSessionName: roleSessionName,
|
|
||||||
RoleTags: roleTags,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package credential
|
package credential
|
||||||
@@ -6,7 +6,7 @@ package credential
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
@@ -25,6 +25,13 @@ func TestGetCredentialAttributes(t *testing.T) {
|
|||||||
in: map[string]any{},
|
in: map[string]any{},
|
||||||
expectedErrContains: "missing required value \"region\"",
|
expectedErrContains: "missing required value \"region\"",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "invalid aws region value",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "dne",
|
||||||
|
},
|
||||||
|
expectedErrContains: "not a valid region",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "bad value for disable_credential_rotation",
|
name: "bad value for disable_credential_rotation",
|
||||||
in: map[string]any{
|
in: map[string]any{
|
||||||
@@ -54,28 +61,6 @@ func TestGetCredentialAttributes(t *testing.T) {
|
|||||||
DisableCredentialRotation: true,
|
DisableCredentialRotation: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "with assume role",
|
|
||||||
in: map[string]any{
|
|
||||||
ConstRegion: "us-west-2",
|
|
||||||
ConstRoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
|
||||||
ConstRoleExternalId: "1234567890",
|
|
||||||
ConstRoleSessionName: "test-session",
|
|
||||||
ConstRoleTags: map[string]interface{}{
|
|
||||||
"foo": "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: &CredentialAttributes{
|
|
||||||
Region: "us-west-2",
|
|
||||||
DisableCredentialRotation: false,
|
|
||||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
|
||||||
RoleExternalId: "1234567890",
|
|
||||||
RoleSessionName: "test-session",
|
|
||||||
RoleTags: map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@@ -104,147 +89,63 @@ func TestGetCredentialAttributes(t *testing.T) {
|
|||||||
func TestGetCredentialsConfig(t *testing.T) {
|
func TestGetCredentialsConfig(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
secrets *structpb.Struct
|
in map[string]any
|
||||||
attrs *CredentialAttributes
|
region string
|
||||||
dualStack bool
|
|
||||||
expected *awsutil.CredentialsConfig
|
expected *awsutil.CredentialsConfig
|
||||||
expectedErrContains string
|
expectedErrContains string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no credentials",
|
name: "missing access_key_id",
|
||||||
attrs: &CredentialAttributes{
|
in: map[string]any{},
|
||||||
Region: "us-west-2",
|
region: "us-west-2",
|
||||||
},
|
expectedErrContains: "missing required value \"access_key_id\"",
|
||||||
expected: &awsutil.CredentialsConfig{
|
|
||||||
Region: "us-west-2",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with static credentials",
|
name: "missing secret_access_key",
|
||||||
secrets: &structpb.Struct{
|
in: map[string]any{
|
||||||
Fields: map[string]*structpb.Value{
|
ConstAccessKeyId: "foobar",
|
||||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
|
||||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
attrs: &CredentialAttributes{
|
|
||||||
Region: "us-west-2",
|
|
||||||
},
|
|
||||||
expected: &awsutil.CredentialsConfig{
|
|
||||||
AccessKey: "AKIAfoobar",
|
|
||||||
SecretKey: "bazqux",
|
|
||||||
Region: "us-west-2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
expected: &awsutil.CredentialsConfig{
|
|
||||||
AccessKey: "AKIAfoobar",
|
|
||||||
SecretKey: "bazqux",
|
|
||||||
Region: "us-west-2",
|
|
||||||
},
|
},
|
||||||
|
region: "us-west-2",
|
||||||
|
expectedErrContains: "missing required value \"secret_access_key\"",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unknown fields",
|
name: "unknown fields",
|
||||||
secrets: &structpb.Struct{
|
in: map[string]any{
|
||||||
Fields: map[string]*structpb.Value{
|
ConstAccessKeyId: "foobar",
|
||||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
ConstSecretAccessKey: "bazqux",
|
||||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
"foo": true,
|
||||||
"foo": structpb.NewBoolValue(true),
|
"bar": true,
|
||||||
"bar": structpb.NewBoolValue(true),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
attrs: &CredentialAttributes{
|
|
||||||
Region: "us-west-2",
|
|
||||||
},
|
},
|
||||||
|
region: "us-west-2",
|
||||||
expectedErrContains: "secrets.bar: unrecognized field, secrets.foo: unrecognized field",
|
expectedErrContains: "secrets.bar: unrecognized field, secrets.foo: unrecognized field",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "valid ignore creds_last_rotated_time",
|
name: "valid ignore creds_last_rotated_time",
|
||||||
secrets: &structpb.Struct{
|
in: map[string]any{
|
||||||
Fields: map[string]*structpb.Value{
|
ConstAccessKeyId: "foobar",
|
||||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
ConstSecretAccessKey: "bazqux",
|
||||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
ConstCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
|
||||||
ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
attrs: &CredentialAttributes{
|
|
||||||
Region: "us-west-2",
|
|
||||||
},
|
},
|
||||||
|
region: "us-west-2",
|
||||||
expected: &awsutil.CredentialsConfig{
|
expected: &awsutil.CredentialsConfig{
|
||||||
AccessKey: "AKIAfoobar",
|
AccessKey: "foobar",
|
||||||
SecretKey: "bazqux",
|
SecretKey: "bazqux",
|
||||||
Region: "us-west-2",
|
Region: "us-west-2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with assume role",
|
name: "good",
|
||||||
secrets: &structpb.Struct{
|
in: map[string]any{
|
||||||
Fields: map[string]*structpb.Value{},
|
ConstAccessKeyId: "foobar",
|
||||||
},
|
ConstSecretAccessKey: "bazqux",
|
||||||
attrs: &CredentialAttributes{
|
|
||||||
Region: "us-west-2",
|
|
||||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
|
||||||
RoleExternalId: "1234567890",
|
|
||||||
RoleSessionName: "test-session",
|
|
||||||
RoleTags: map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
},
|
|
||||||
DisableCredentialRotation: true,
|
|
||||||
},
|
},
|
||||||
|
region: "us-west-2",
|
||||||
expected: &awsutil.CredentialsConfig{
|
expected: &awsutil.CredentialsConfig{
|
||||||
Region: "us-west-2",
|
AccessKey: "foobar",
|
||||||
RoleARN: "arn:aws:iam::123456789012:role/S3Access",
|
SecretKey: "bazqux",
|
||||||
RoleExternalId: "1234567890",
|
Region: "us-west-2",
|
||||||
RoleSessionName: "test-session",
|
|
||||||
RoleTags: map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "with static credential & assume role",
|
|
||||||
secrets: &structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
ConstAccessKeyId: structpb.NewStringValue("AKIAfoobar"),
|
|
||||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
attrs: &CredentialAttributes{
|
|
||||||
Region: "us-west-2",
|
|
||||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
|
||||||
RoleExternalId: "1234567890",
|
|
||||||
RoleSessionName: "test-session",
|
|
||||||
RoleTags: map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedErrContains: "attributes.role_arn: conflicts with access_key_id and secret_access_key values, secrets.access_key_id: conflicts with role_arn value, secrets.secret_access_key: conflicts with role_arn value",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with dynamic credentials and no disable credential rotation",
|
|
||||||
secrets: &structpb.Struct{Fields: map[string]*structpb.Value{}},
|
|
||||||
attrs: &CredentialAttributes{
|
|
||||||
Region: "us-west-2",
|
|
||||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
|
||||||
RoleExternalId: "1234567890",
|
|
||||||
RoleSessionName: "test-session",
|
|
||||||
RoleTags: map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedErrContains: "attributes.disable_credential_rotation: disable_credential_rotation attribute is required when providing a role_arn",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@@ -252,7 +153,10 @@ func TestGetCredentialsConfig(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
actual, err := GetCredentialsConfig(tc.secrets, tc.attrs, tc.dualStack)
|
input, err := structpb.NewStruct(tc.in)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
actual, err := GetCredentialsConfig(input, tc.region)
|
||||||
if tc.expectedErrContains != "" {
|
if tc.expectedErrContains != "" {
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
require.Contains(err.Error(), tc.expectedErrContains)
|
require.Contains(err.Error(), tc.expectedErrContains)
|
||||||
@@ -264,17 +168,6 @@ func TestGetCredentialsConfig(t *testing.T) {
|
|||||||
require.Equal(tc.expected.AccessKey, actual.AccessKey)
|
require.Equal(tc.expected.AccessKey, actual.AccessKey)
|
||||||
require.Equal(tc.expected.SecretKey, actual.SecretKey)
|
require.Equal(tc.expected.SecretKey, actual.SecretKey)
|
||||||
require.Equal(tc.expected.Region, actual.Region)
|
require.Equal(tc.expected.Region, actual.Region)
|
||||||
require.Equal(tc.expected.RoleARN, actual.RoleARN)
|
|
||||||
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 IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package credential
|
package credential
|
||||||
@@ -18,16 +18,4 @@ const (
|
|||||||
|
|
||||||
// ConstCredsLastRotatedTime is the key for the last rotated time in the aws credentials.
|
// ConstCredsLastRotatedTime is the key for the last rotated time in the aws credentials.
|
||||||
ConstCredsLastRotatedTime = "creds_last_rotated_time"
|
ConstCredsLastRotatedTime = "creds_last_rotated_time"
|
||||||
|
|
||||||
// ConstRoleArn is the key for assuming a IAM role.
|
|
||||||
ConstRoleArn = "role_arn"
|
|
||||||
|
|
||||||
// ConstRoleExternalId is the key for the external id used for assuming a IAM role.
|
|
||||||
ConstRoleExternalId = "role_external_id"
|
|
||||||
|
|
||||||
// ConstRoleSessionName is the key for the session name used for assuming a IAM role.
|
|
||||||
ConstRoleSessionName = "role_session_name"
|
|
||||||
|
|
||||||
// ConstRoleTags is the key for the tags used for assuming a IAM role.
|
|
||||||
ConstRoleTags = "role_tags"
|
|
||||||
)
|
)
|
||||||
+148
-207
@@ -1,48 +1,21 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package credential
|
package credential
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"errors"
|
||||||
stderr "errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
"github.com/aws/aws-sdk-go/service/iam"
|
||||||
"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/boundary-plugin-aws/internal/values"
|
||||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CredentialType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// StaticAWS denotes an Access Key Id that begins with "AKIA". These are
|
|
||||||
// long-term access keys, provided by AWS, for an IAM user or an AWS account
|
|
||||||
// root user.
|
|
||||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html#sec-access-keys-and-secret-access-keys
|
|
||||||
StaticAWS CredentialType = iota
|
|
||||||
|
|
||||||
// DynamicAWS denotes the presence of a RoleARN, or an Access Key Id that
|
|
||||||
// begins with "ASIA". The latter are temporary credentials access keys that
|
|
||||||
// are created using AWS STS operations.
|
|
||||||
DynamicAWS
|
|
||||||
|
|
||||||
// StaticOther denotes the presence of an Access Key Id that does not follow
|
|
||||||
// the AKIA/ASIA convention outlined by AWS.
|
|
||||||
StaticOther
|
|
||||||
|
|
||||||
// Unknown is a catch-all for everything else.
|
|
||||||
Unknown
|
|
||||||
)
|
|
||||||
|
|
||||||
// rotationWaitTimeout controls the time we wait for credential rotation to
|
// rotationWaitTimeout controls the time we wait for credential rotation to
|
||||||
// succeed. This is important to ensure that rotated credentials can be used
|
// succeed. This is important to ensure that rotated credentials can be used
|
||||||
// right away.
|
// right away.
|
||||||
@@ -52,13 +25,19 @@ type AwsCredentialPersistedStateOption func(s *AwsCredentialPersistedState) erro
|
|||||||
|
|
||||||
// AwsCredentialPersistedState is the persisted state for the AWS credential.
|
// AwsCredentialPersistedState is the persisted state for the AWS credential.
|
||||||
type AwsCredentialPersistedState struct {
|
type AwsCredentialPersistedState struct {
|
||||||
// CredentialsConfig is the credential configuration for the AWS credential.
|
// AccessKeyId is the access key id for the AWS credential.
|
||||||
CredentialsConfig *awsutil.CredentialsConfig
|
AccessKeyId string
|
||||||
|
// SecretAccessKey is the secret access key for the AWS credential.
|
||||||
|
SecretAccessKey string
|
||||||
// CredsLastRotatedTime is the last rotation of aws secrets for the AWS credential.
|
// CredsLastRotatedTime is the last rotation of aws secrets for the AWS credential.
|
||||||
CredsLastRotatedTime time.Time
|
CredsLastRotatedTime time.Time
|
||||||
|
|
||||||
// testOpts are options that should be used for testing only
|
// testOpts are options that should be used for testing only
|
||||||
testOpts []awsutil.Option
|
testOpts []awsutil.Option
|
||||||
|
|
||||||
|
// Region is not a part of persisted state but if set is passed to clients
|
||||||
|
// created with this state
|
||||||
|
region string
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithStateTestOpts enables unit testing different edge cases
|
// WithStateTestOpts enables unit testing different edge cases
|
||||||
@@ -71,23 +50,35 @@ func WithStateTestOpts(opts []awsutil.Option) AwsCredentialPersistedStateOption
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithCredentialsConfig sets the value for CredentialsConfig in the credential persisted state.
|
// WithAccessKeyId sets the value for AccesskeyId in the storage persisted state.
|
||||||
func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedStateOption {
|
func WithAccessKeyId(x string) AwsCredentialPersistedStateOption {
|
||||||
return func(s *AwsCredentialPersistedState) error {
|
return func(s *AwsCredentialPersistedState) error {
|
||||||
if s.CredentialsConfig != nil {
|
if s.AccessKeyId != "" {
|
||||||
return stderr.New("credentials config already set")
|
return errors.New("access key id already set")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.CredentialsConfig = x
|
s.AccessKeyId = x
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithCredsLastRotatedTime sets the value for CredsLastRotatedTime in the credential persisted state.
|
// WithSecretAccessKey sets the value for SecretAccessKey in the storage persisted state.
|
||||||
|
func WithSecretAccessKey(x string) AwsCredentialPersistedStateOption {
|
||||||
|
return func(s *AwsCredentialPersistedState) error {
|
||||||
|
if s.SecretAccessKey != "" {
|
||||||
|
return errors.New("secret access key already set")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.SecretAccessKey = x
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCredsLastRotatedTime sets the value for CredsLastRotatedTime in the storage persisted state.
|
||||||
func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
|
func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
|
||||||
return func(s *AwsCredentialPersistedState) error {
|
return func(s *AwsCredentialPersistedState) error {
|
||||||
if !s.CredsLastRotatedTime.IsZero() {
|
if !s.CredsLastRotatedTime.IsZero() {
|
||||||
return stderr.New("last rotation time already set")
|
return errors.New("last rotation time already set")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.CredsLastRotatedTime = t
|
s.CredsLastRotatedTime = t
|
||||||
@@ -95,6 +86,18 @@ func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithRegion sets the value for region in the storage persisted state.
|
||||||
|
func WithRegion(with string) AwsCredentialPersistedStateOption {
|
||||||
|
return func(s *AwsCredentialPersistedState) error {
|
||||||
|
if s.region != "" {
|
||||||
|
return errors.New("region already set")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.region = with
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewAwsCredentialPersistedState returns a AwsCredentialPersistedState.
|
// NewAwsCredentialPersistedState returns a AwsCredentialPersistedState.
|
||||||
// Supported options include: WithAccessKeyId, WithSecretAccessKey
|
// Supported options include: WithAccessKeyId, WithSecretAccessKey
|
||||||
// WithCredsLastRotatedTime, & WithRegion.
|
// WithCredsLastRotatedTime, & WithRegion.
|
||||||
@@ -105,21 +108,27 @@ func NewAwsCredentialPersistedState(opts ...AwsCredentialPersistedStateOption) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateCreds takes the credentials configuration from the persisted state
|
// ValidateCreds takes the access key and secret key from the persisted state
|
||||||
// and runs sts.GetCallerIdentity for the current credentials, which is done
|
// and runs sts.GetCallerIdentity for the current credentials, which is done
|
||||||
// to check that the credentials are valid. This method returns a status error
|
// to check that the credentials are valid.
|
||||||
// with PluginError details.
|
func (s *AwsCredentialPersistedState) ValidateCreds() error {
|
||||||
func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
|
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||||
if s.CredentialsConfig == nil {
|
awsutil.WithAccessKey(s.AccessKeyId),
|
||||||
return errors.BadRequestStatus("missing credentials config")
|
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||||
|
awsutil.WithRegion(s.region),
|
||||||
|
}, s.testOpts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error loading credentials: %w", err)
|
||||||
}
|
}
|
||||||
if _, err := s.CredentialsConfig.GetCallerIdentity(ctx, s.testOpts...); err != nil {
|
|
||||||
st, _ := errors.ParseAWSError("validating credentials", err)
|
if _, err := c.GetCallerIdentity(s.testOpts...); err != nil {
|
||||||
return st.Err()
|
return fmt.Errorf("error validating credentials: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,139 +136,147 @@ func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
|
|||||||
// then deletes the old access key. If deletion of the old access key is successful, the new access key/secret key are
|
// then deletes the old access key. If deletion of the old access key is successful, the new access key/secret key are
|
||||||
// written into the credentials config and the persisted state. On any error, the old credentials are not overwritten.
|
// written into the credentials config and the persisted state. On any error, the old credentials are not overwritten.
|
||||||
// This ensures that any generated new secret key never leaves this function in case of an error, even though it will
|
// This ensures that any generated new secret key never leaves this function in case of an error, even though it will
|
||||||
// still result in an extraneous access key existing. This method returns a status error with PluginError details.
|
// still result in an extraneous access key existing.
|
||||||
func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error {
|
func (s *AwsCredentialPersistedState) RotateCreds() error {
|
||||||
if s.CredentialsConfig == nil {
|
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||||
return errors.BadRequestStatus("missing credentials config")
|
awsutil.WithAccessKey(s.AccessKeyId),
|
||||||
|
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||||
|
awsutil.WithRegion(s.region),
|
||||||
|
}, s.testOpts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error loading credentials: %w", err)
|
||||||
}
|
}
|
||||||
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
|
|
||||||
return errors.BadRequestStatus("invalid credential type")
|
if err := c.RotateKeys(append([]awsutil.Option{
|
||||||
}
|
|
||||||
if err := s.CredentialsConfig.RotateKeys(ctx, append([]awsutil.Option{
|
|
||||||
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
|
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
|
||||||
}, s.testOpts...)...); err != nil {
|
}, s.testOpts...)...); err != nil {
|
||||||
st, _ := errors.ParseAWSError("rotating credentials", err)
|
return fmt.Errorf("error rotating credentials: %w", err)
|
||||||
return st.Err()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.AccessKeyId = c.AccessKey
|
||||||
|
s.SecretAccessKey = c.SecretKey
|
||||||
s.CredsLastRotatedTime = time.Now()
|
s.CredsLastRotatedTime = time.Now()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplaceCreds replaces the access key in the state with a new key.
|
// ReplaceCreds replaces the access key in the state with a new key.
|
||||||
// If the existing key was rotated at any point in time, it is
|
// If the existing key was rotated at any point in time, it is
|
||||||
// deleted first, otherwise it's left alone. This method returns a
|
// deleted first, otherwise it's left alone.
|
||||||
// status error with PluginError details.
|
func (s *AwsCredentialPersistedState) ReplaceCreds(accessKeyId, secretAccessKey string) error {
|
||||||
func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credentialsConfig *awsutil.CredentialsConfig) error {
|
if accessKeyId == "" {
|
||||||
if credentialsConfig == nil {
|
return errors.New("access key id cannot be empty")
|
||||||
return errors.BadRequestStatus("missing new credentials config")
|
|
||||||
}
|
|
||||||
if s.CredentialsConfig == nil {
|
|
||||||
return errors.BadRequestStatus("missing credentials config")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete old/existing credentials. This action is only possible for static credential types.
|
if secretAccessKey == "" {
|
||||||
// This is done with the same credentials to ensure that it has the proper permissions to do it.
|
return errors.New("secret access key cannot be empty")
|
||||||
ct := GetCredentialType(s.CredentialsConfig)
|
}
|
||||||
if !s.CredsLastRotatedTime.IsZero() && ct == StaticAWS {
|
|
||||||
if err := s.DeleteCreds(ctx); err != nil {
|
if accessKeyId == s.AccessKeyId {
|
||||||
|
return errors.New("attempting to replace access key with the same one")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.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 := s.DeleteCreds(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the new attributes and clear the rotated time.
|
// Set the new attributes and clear the rotated time.
|
||||||
s.CredentialsConfig = credentialsConfig
|
s.AccessKeyId = accessKeyId
|
||||||
|
s.SecretAccessKey = secretAccessKey
|
||||||
s.CredsLastRotatedTime = time.Time{}
|
s.CredsLastRotatedTime = time.Time{}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCreds deletes the credentials in the state. The access key
|
// DeleteCreds deletes the credentials in the state. The access key
|
||||||
// ID, secret access key, and rotation time fields are zeroed out in
|
// ID, secret access key, and rotation time fields are zeroed out in
|
||||||
// the state just to ensure that they cannot be re-used after. This
|
// the state just to ensure that they cannot be re-used after.
|
||||||
// method returns a status error with PluginError details.
|
func (s *AwsCredentialPersistedState) DeleteCreds() error {
|
||||||
func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error {
|
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||||
if s.CredentialsConfig == nil {
|
awsutil.WithAccessKey(s.AccessKeyId),
|
||||||
return errors.BadRequestStatus("missing credentials config")
|
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||||
}
|
awsutil.WithRegion(s.region),
|
||||||
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
|
}, s.testOpts...)...)
|
||||||
return errors.BadRequestStatus("invalid credential type")
|
if err != nil {
|
||||||
|
return fmt.Errorf("error loading credentials: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.CredentialsConfig.DeleteAccessKey(ctx, s.CredentialsConfig.AccessKey, s.testOpts...); err != nil {
|
if err := c.DeleteAccessKey(s.AccessKeyId, s.testOpts...); err != nil {
|
||||||
// Determine if the deletion error was due to a missing
|
// Determine if the deletion error was due to a missing
|
||||||
// resource. If it was, just pass it.
|
// resource. If it was, just pass it.
|
||||||
var awsErr *iamTypes.NoSuchEntityException
|
var awsErr awserr.Error
|
||||||
if stderr.As(err, &awsErr) && awsErr.ErrorCode() == "NoSuchEntity" {
|
if errors.As(err, &awsErr) {
|
||||||
s.CredentialsConfig = nil
|
if awsErr.Code() == iam.ErrCodeNoSuchEntityException {
|
||||||
s.CredsLastRotatedTime = time.Time{}
|
s.AccessKeyId = ""
|
||||||
return nil
|
s.SecretAccessKey = ""
|
||||||
|
s.CredsLastRotatedTime = time.Time{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise treat it as an actual error.
|
// Otherwise treat it as an actual error.
|
||||||
st, _ := errors.ParseAWSError("deleting credentials", err)
|
return err
|
||||||
return st.Err()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.CredentialsConfig = nil
|
s.AccessKeyId = ""
|
||||||
|
s.SecretAccessKey = ""
|
||||||
s.CredsLastRotatedTime = time.Time{}
|
s.CredsLastRotatedTime = time.Time{}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateCredentialChain returns a AWS configuration for the credentials in the state.
|
// GetSession returns a configured AWS session for the credentials in
|
||||||
func (s *AwsCredentialPersistedState) GenerateCredentialChain(ctx context.Context) (*aws.Config, error) {
|
// the state.
|
||||||
return s.CredentialsConfig.GenerateCredentialChain(ctx, s.testOpts...)
|
func (s *AwsCredentialPersistedState) GetSession() (*session.Session, error) {
|
||||||
|
c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{
|
||||||
|
awsutil.WithAccessKey(s.AccessKeyId),
|
||||||
|
awsutil.WithSecretKey(s.SecretAccessKey),
|
||||||
|
awsutil.WithRegion(s.region),
|
||||||
|
}, s.testOpts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.GetSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMap returns a map of the credentials stored in the persisted state.
|
// ToMap returns a map of the credentials stored in the persisted state,
|
||||||
// ToMap will return an empty map for temporary credentials. ToMap will
|
// which includes the following keys: access_key_id, secret_access_key,
|
||||||
// return a map for long-term credentials with following keys:
|
// & creds_last_rotated_time
|
||||||
// access_key_id, secret_access_key & creds_last_rotated_time
|
|
||||||
func (s *AwsCredentialPersistedState) ToMap() map[string]any {
|
func (s *AwsCredentialPersistedState) ToMap() map[string]any {
|
||||||
ct := GetCredentialType(s.CredentialsConfig)
|
|
||||||
// Dynamic AWS credentials are temporary credentials that expire within an hour.
|
|
||||||
// ToMap() returns an empty map here so that Boundary does not store temporary
|
|
||||||
// credentials into the database.
|
|
||||||
//
|
|
||||||
// Uknown credentials are not aws compatiable secrets that may have been provided
|
|
||||||
// for a non-aws s3 compatiable service. These credentials will not be returned
|
|
||||||
// because the plugin does not know how to manage non-aws credentials.
|
|
||||||
if ct == DynamicAWS || ct == Unknown {
|
|
||||||
return map[string]any{}
|
|
||||||
}
|
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
ConstAccessKeyId: s.CredentialsConfig.AccessKey,
|
ConstAccessKeyId: s.AccessKeyId,
|
||||||
ConstSecretAccessKey: s.CredentialsConfig.SecretKey,
|
ConstSecretAccessKey: s.SecretAccessKey,
|
||||||
ConstCredsLastRotatedTime: s.CredsLastRotatedTime.Format(time.RFC3339Nano),
|
ConstCredsLastRotatedTime: s.CredsLastRotatedTime.Format(time.RFC3339Nano),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AwsCredentialPersistedStateFromProto parses values out of a protobuf struct input
|
// AwsCredentialPersistedStateFromProto parses values out of a protobuf struct input
|
||||||
// and returns a AwsCredentialPersistedState used for configuring an AWS session.
|
// and returns a AwsCredentialPersistedState used for configuring an AWS session.
|
||||||
func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, dualStack bool, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
|
// An error is returned if any of the following fields are missing from the protobuf
|
||||||
// initialize secrets if it is nil
|
// struct input or have invalid value types: access_key_id, secret_access_key, &
|
||||||
// secrets can be nil because static credentials are optional
|
// creds_last_rotated_time.
|
||||||
if secrets == nil {
|
func AwsCredentialPersistedStateFromProto(in *structpb.Struct, opts ...AwsCredentialPersistedStateOption) (*AwsCredentialPersistedState, error) {
|
||||||
secrets = &structpb.Struct{
|
if in == nil {
|
||||||
Fields: map[string]*structpb.Value{},
|
return nil, errors.New("missing persisted secrets")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if attrs == nil {
|
accessKeyId, err := values.GetStringValue(in, ConstAccessKeyId, true)
|
||||||
return nil, fmt.Errorf("missing credential attributes")
|
|
||||||
}
|
|
||||||
|
|
||||||
accessKeyId, err := values.GetStringValue(secrets, ConstAccessKeyId, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("persisted state integrity: %w", err)
|
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secretAccessKey, err := values.GetStringValue(secrets, ConstSecretAccessKey, false)
|
secretAccessKey, err := values.GetStringValue(in, ConstSecretAccessKey, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("persisted state integrity: %w", err)
|
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
credsLastRotatedTime, err := values.GetTimeValue(secrets, ConstCredsLastRotatedTime)
|
credsLastRotatedTime, err := values.GetTimeValue(in, ConstCredsLastRotatedTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("persisted state integrity: %w", err)
|
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := NewAwsCredentialPersistedState(opts...)
|
s, err := NewAwsCredentialPersistedState(opts...)
|
||||||
@@ -267,85 +284,9 @@ func AwsCredentialPersistedStateFromProto(secrets *structpb.Struct, attrs *Crede
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
awsOpts := append([]awsutil.Option{}, s.testOpts...)
|
s.AccessKeyId = accessKeyId
|
||||||
if accessKeyId != "" && secretAccessKey != "" {
|
s.SecretAccessKey = secretAccessKey
|
||||||
awsOpts = append(awsOpts,
|
|
||||||
awsutil.WithAccessKey(accessKeyId),
|
|
||||||
awsutil.WithSecretKey(secretAccessKey),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if attrs.Region != "" {
|
|
||||||
awsOpts = append(awsOpts, awsutil.WithRegion(attrs.Region))
|
|
||||||
}
|
|
||||||
if attrs.RoleArn != "" {
|
|
||||||
awsOpts = append(awsOpts, awsutil.WithRoleArn(attrs.RoleArn))
|
|
||||||
}
|
|
||||||
if attrs.RoleExternalId != "" {
|
|
||||||
awsOpts = append(awsOpts, awsutil.WithRoleExternalId(attrs.RoleExternalId))
|
|
||||||
}
|
|
||||||
if attrs.RoleSessionName != "" {
|
|
||||||
awsOpts = append(awsOpts, awsutil.WithRoleSessionName(attrs.RoleSessionName))
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
s.CredentialsConfig = credentialsConfig
|
|
||||||
s.CredsLastRotatedTime = credsLastRotatedTime
|
s.CredsLastRotatedTime = credsLastRotatedTime
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCredentialType returns the credential type based on the given
|
|
||||||
// AccessKey/RoleARN. See CredentialType definition for more information.
|
|
||||||
func GetCredentialType(cc *awsutil.CredentialsConfig) CredentialType {
|
|
||||||
if cc == nil {
|
|
||||||
return Unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cc.RoleARN) > 0 || strings.HasPrefix(cc.AccessKey, "ASIA") {
|
|
||||||
return DynamicAWS
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(cc.AccessKey, "AKIA") {
|
|
||||||
return StaticAWS
|
|
||||||
}
|
|
||||||
if len(cc.AccessKey) > 0 {
|
|
||||||
return StaticOther
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
+216
-535
File diff suppressed because it is too large.
Load diff
@@ -1,15 +1,13 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package credential
|
package credential
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/iam"
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go/service/iam/iamiface"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -28,61 +26,25 @@ func (s *testMockIAMState) Reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type testMockIAM struct {
|
type testMockIAM struct {
|
||||||
awsutil.IAMClient
|
iamiface.IAMAPI
|
||||||
|
|
||||||
State *testMockIAMState
|
State *testMockIAMState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *testMockIAM) DeleteAccessKey(ctx context.Context, input *iam.DeleteAccessKeyInput, opts ...func(*iam.Options)) (*iam.DeleteAccessKeyOutput, error) {
|
func (m *testMockIAM) DeleteAccessKey(input *iam.DeleteAccessKeyInput) (*iam.DeleteAccessKeyOutput, error) {
|
||||||
m.State.DeleteAccessKeyCalled = true
|
m.State.DeleteAccessKeyCalled = true
|
||||||
return m.IAMClient.DeleteAccessKey(ctx, input, opts...)
|
return m.IAMAPI.DeleteAccessKey(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestMockIAM(state *testMockIAMState, opts ...awsutil.MockIAMOption) awsutil.IAMAPIFunc {
|
func newTestMockIAM(state *testMockIAMState, opts ...awsutil.MockIAMOption) awsutil.IAMAPIFunc {
|
||||||
return func(awsConfig *aws.Config) (awsutil.IAMClient, error) {
|
return func(sess *session.Session) (iamiface.IAMAPI, error) {
|
||||||
m := &testMockIAM{
|
m := &testMockIAM{
|
||||||
State: state,
|
State: state,
|
||||||
}
|
}
|
||||||
f := awsutil.NewMockIAM(opts...)
|
f := awsutil.NewMockIAM(opts...)
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
m.IAMClient, err = f(awsConfig)
|
m.IAMAPI, err = f(sess)
|
||||||
return m, err
|
return m, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockStaticCredentialSecrets returns a *structpb.Struct that contains two
|
|
||||||
// key pair values: (access_key_id, AKIA_foobar) & (secret_access_key, bazqux)
|
|
||||||
func MockStaticCredentialSecrets() *structpb.Struct {
|
|
||||||
return &structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
|
||||||
ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockAssumeRoleAttributes returns a *structpb.Struct that contains six key pair values:
|
|
||||||
//
|
|
||||||
// (region, region)
|
|
||||||
// (disable_credential_rotation, disableRotate)
|
|
||||||
// (role_arn, arn:aws:iam::123456789012:role/S3Access)
|
|
||||||
// (role_external_id, 1234567890)
|
|
||||||
// (role_session_name, ec2-assume-role-provider)
|
|
||||||
// (role_tags, struct{foo:bar})
|
|
||||||
func MockAssumeRoleAttributes(region string, disableRotate bool) *structpb.Struct {
|
|
||||||
return &structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
ConstRegion: structpb.NewStringValue(region),
|
|
||||||
ConstDisableCredentialRotation: structpb.NewBoolValue(disableRotate),
|
|
||||||
ConstRoleArn: structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
|
|
||||||
ConstRoleExternalId: structpb.NewStringValue("1234567890"),
|
|
||||||
ConstRoleSessionName: structpb.NewStringValue("ec2-assume-role-provider"),
|
|
||||||
ConstRoleTags: structpb.NewStructValue(&structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
"foo": structpb.NewStringValue("bar"),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-258
@@ -1,71 +1,15 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package errors
|
package errors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// awsErrorAccessDenied is returned when the credentials do not have
|
|
||||||
// the required permissions to do a specific action on a resource.
|
|
||||||
// This error will persist until the IAM Role Policy is updated by
|
|
||||||
// the owner. This error cannot be resolved by retrying the request.
|
|
||||||
awsErrorAccessDenied = "AccessDenied"
|
|
||||||
|
|
||||||
// awsErrorInvalidAccessKeyId is returned when the credential does
|
|
||||||
// not exist. This error will persist until the credentials attached
|
|
||||||
// to the storage bucket is updated. This error can occur when the
|
|
||||||
// given credentials are deleted, deactivated, or rotated. This
|
|
||||||
// error cannot be resolved by retrying the request.
|
|
||||||
awsErrorInvalidAccessKeyId = "InvalidAccessKeyId"
|
|
||||||
|
|
||||||
// awsErrorExpiredToken is returned when the provided credential is
|
|
||||||
// expired. This error can only be returned by a dynamic credential
|
|
||||||
// type. This error can resolve itself by retrying the request.
|
|
||||||
awsErrorExpiredToken = "ExpiredToken"
|
|
||||||
|
|
||||||
// NoSuchBucket is returned when the specified S3 storage bucket does
|
|
||||||
// not exist
|
|
||||||
awsErrorNoSuchBucket = "NoSuchBucket"
|
|
||||||
|
|
||||||
// BadDigest is returned when an S3 PUT OBJECT request's calculated
|
|
||||||
// checksum does not match the checksum which was sent by the client
|
|
||||||
awsErrorBadDigest = "BadDigest"
|
|
||||||
|
|
||||||
// NoSuchKey is returned when attempting to interact with an S3 object
|
|
||||||
// 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"
|
|
||||||
|
|
||||||
// RequestTimeout is returned when an http request takes longer than allowed
|
|
||||||
awsErrorRequestTimeout = "RequestTimeout"
|
|
||||||
|
|
||||||
// RequestTimeoutException is returned when an http request takes longer than allowed
|
|
||||||
awsErrorRequestTimeoutException = "RequestTimeoutException"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// InvalidArgumentError returns an grpc invalid argument status error.
|
// InvalidArgumentError returns an grpc invalid argument status error.
|
||||||
@@ -78,205 +22,5 @@ func InvalidArgumentError(msg string, f map[string]string) error {
|
|||||||
sort.Strings(fieldMsgs)
|
sort.Strings(fieldMsgs)
|
||||||
msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", "))
|
msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", "))
|
||||||
}
|
}
|
||||||
return BadRequestStatus(msg)
|
return status.Error(codes.InvalidArgument, msg)
|
||||||
}
|
|
||||||
|
|
||||||
// ParseAWSError converts an aws service error into a RPC status. This method
|
|
||||||
// will handle parsing the following aws error codes: throttling, connections,
|
|
||||||
// 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(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
|
|
||||||
if errors.As(err, &oe) {
|
|
||||||
serviceName = oe.Service()
|
|
||||||
}
|
|
||||||
|
|
||||||
// The unknown plugin error code will be used
|
|
||||||
// when the aws service returns an error that
|
|
||||||
// has failed to be parsed. By default, this
|
|
||||||
// error code is retryable.
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if st == nil {
|
|
||||||
statusMsg := fmt.Sprintf("aws service %s: unknown error: %s", serviceName, msg)
|
|
||||||
st = status.New(codes.Unknown, statusMsg)
|
|
||||||
}
|
|
||||||
if permission == nil {
|
|
||||||
permission = &pb.Permission{State: pb.StateType_STATE_TYPE_UNKNOWN, CheckedAt: timestamppb.Now()}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// evaluate against aws throttling error codes
|
|
||||||
throttleErr := retry.ThrottleErrorCode{
|
|
||||||
Codes: retry.DefaultThrottleErrorCodes,
|
|
||||||
}
|
|
||||||
if throttleErr.IsErrorThrottle(err).Bool() {
|
|
||||||
statusMsg := fmt.Sprintf("aws service %s: throttling: %s", serviceName, msg)
|
|
||||||
return status.New(codes.Unavailable, statusMsg), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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() {
|
|
||||||
case awsErrorAccessDenied:
|
|
||||||
fallthrough
|
|
||||||
case awsErrorInvalidAccessKeyId:
|
|
||||||
fallthrough
|
|
||||||
case awsErrorExpiredToken:
|
|
||||||
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 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 service %s: %s", serviceName, msg)
|
|
||||||
return status.New(codes.Aborted, statusMsg), nil
|
|
||||||
case awsErrorNoSuchKey:
|
|
||||||
fallthrough
|
|
||||||
case awsErrorNotFound:
|
|
||||||
fallthrough
|
|
||||||
case awsErrorInvalidObjectState:
|
|
||||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
|
||||||
return status.New(codes.NotFound, statusMsg), nil
|
|
||||||
case awsErrorRequestTimeout:
|
|
||||||
fallthrough
|
|
||||||
case awsErrorRequestTimeoutException:
|
|
||||||
statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg)
|
|
||||||
return status.New(codes.DeadlineExceeded, statusMsg), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
return status.New(codes.Internal, err.Error()), nil
|
|
||||||
}
|
|
||||||
defer httpErr.Response.Body.Close()
|
|
||||||
switch httpErr.HTTPStatusCode() {
|
|
||||||
case http.StatusBadRequest:
|
|
||||||
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: %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: %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: %s", serviceName, msg)
|
|
||||||
return status.New(codes.NotFound, statusMsg), nil
|
|
||||||
case http.StatusTooManyRequests:
|
|
||||||
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: %s", serviceName, msg)
|
|
||||||
return status.New(codes.Unavailable, statusMsg), nil
|
|
||||||
case http.StatusRequestTimeout:
|
|
||||||
fallthrough
|
|
||||||
case http.StatusGatewayTimeout:
|
|
||||||
statusMsg := fmt.Sprintf("aws service %s: timeout: %s", serviceName, msg)
|
|
||||||
return status.New(codes.DeadlineExceeded, statusMsg), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return st, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
@@ -1,427 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package errors
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_ParseAWSError(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
err error
|
|
||||||
expectedStatusCode codes.Code
|
|
||||||
expectedStatusMsg string
|
|
||||||
expectedPermission *pb.Permission
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "nil-error",
|
|
||||||
expectedStatusCode: codes.OK,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "default-to-unknwon",
|
|
||||||
err: fmt.Errorf("what can this error be?"),
|
|
||||||
expectedStatusCode: codes.Unknown,
|
|
||||||
expectedStatusMsg: "aws service unknown: unknown error: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http-status-code-bad-request",
|
|
||||||
err: TestAwsHttpResponseError(http.StatusBadRequest, "bad request"),
|
|
||||||
expectedStatusCode: codes.InvalidArgument,
|
|
||||||
expectedStatusMsg: "aws service unknown: bad request: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http-status-code-unauthorized",
|
|
||||||
err: TestAwsHttpResponseError(http.StatusUnauthorized, "unauthorized request"),
|
|
||||||
expectedStatusCode: codes.PermissionDenied,
|
|
||||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_ERROR,
|
|
||||||
ErrorDetails: "unauthorized request",
|
|
||||||
CheckedAt: timestamppb.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http-status-code-forbidden",
|
|
||||||
err: 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: "http-status-code-not-found",
|
|
||||||
err: TestAwsHttpResponseError(http.StatusNotFound, "not found request"),
|
|
||||||
expectedStatusCode: codes.NotFound,
|
|
||||||
expectedStatusMsg: "aws service unknown: resource not found: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http-status-code-too-many-requests",
|
|
||||||
err: TestAwsHttpResponseError(http.StatusTooManyRequests, "too many requests"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http-status-code-request-timeout",
|
|
||||||
err: TestAwsHttpResponseError(http.StatusRequestTimeout, "request timeout"),
|
|
||||||
expectedStatusCode: codes.DeadlineExceeded,
|
|
||||||
expectedStatusMsg: "aws service unknown: timeout: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http-status-code-internal-server-error",
|
|
||||||
err: TestAwsHttpResponseError(http.StatusInternalServerError, "internal server error"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: connectivity: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http-status-code-bad-gateway",
|
|
||||||
err: TestAwsHttpResponseError(http.StatusBadGateway, "bad gateway"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: connectivity: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http-status-code-service-unavailable",
|
|
||||||
err: TestAwsHttpResponseError(http.StatusServiceUnavailable, "service unavailable"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: connectivity: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http-status-code-gateway-timeout",
|
|
||||||
err: TestAwsHttpResponseError(http.StatusGatewayTimeout, "gateway timeout"),
|
|
||||||
expectedStatusCode: codes.DeadlineExceeded,
|
|
||||||
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: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-throttling-exception",
|
|
||||||
err: TestAwsError("ThrottlingException", "throttling exception"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-throttled-exception",
|
|
||||||
err: TestAwsError("ThrottledException", "throttled exception"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-throttled-exception",
|
|
||||||
err: TestAwsError("RequestThrottledException", "request throttled exception"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-too-many-requests-exception",
|
|
||||||
err: TestAwsError("TooManyRequestsException", "too many requests exception"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-provisioned-throughput-exceeded-exception",
|
|
||||||
err: TestAwsError("ProvisionedThroughputExceededException", "provisioned throughput exceeded exception"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-transaction-in-progress-exception",
|
|
||||||
err: TestAwsError("TransactionInProgressException", "transaction in progress exception"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-limit-exceeded",
|
|
||||||
err: TestAwsError("RequestLimitExceeded", "request limit exceeded"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-bandwidth-limit-exceeded",
|
|
||||||
err: TestAwsError("BandwidthLimitExceeded", "bandwidth limit exceeded"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-limit-exceeded-exception",
|
|
||||||
err: TestAwsError("LimitExceededException", "limit exceeded exception"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-throttled",
|
|
||||||
err: TestAwsError("RequestThrottled", "request throttled"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-slow-down",
|
|
||||||
err: TestAwsError("SlowDown", "slow down"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-prior-request-not-complete",
|
|
||||||
err: TestAwsError("PriorRequestNotComplete", "prior request not complete"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-ec2-throttled-exception",
|
|
||||||
err: TestAwsError("EC2ThrottledException", "ec2 throttled exception"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service unknown: throttling: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-timeout",
|
|
||||||
err: TestAwsError("RequestTimeout", "request timeout"),
|
|
||||||
expectedStatusCode: codes.DeadlineExceeded,
|
|
||||||
expectedStatusMsg: "aws service unknown: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-timeout-exception",
|
|
||||||
err: TestAwsError("RequestTimeoutException", "request timeout exception"),
|
|
||||||
expectedStatusCode: codes.DeadlineExceeded,
|
|
||||||
expectedStatusMsg: "aws service unknown: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aws-request-access-denied",
|
|
||||||
err: TestAwsError(awsErrorAccessDenied, "not authorized to perform action"),
|
|
||||||
expectedStatusCode: codes.PermissionDenied,
|
|
||||||
expectedStatusMsg: "aws service unknown: invalid credentials: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_ERROR,
|
|
||||||
ErrorDetails: "not authorized to perform action",
|
|
||||||
CheckedAt: timestamppb.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: test",
|
|
||||||
expectedPermission: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_ERROR,
|
|
||||||
ErrorDetails: "The AWS Access Key Id you provided does not exist in our records.",
|
|
||||||
CheckedAt: timestamppb.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: 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("test", tt.err)
|
|
||||||
if tt.expectedStatusCode == codes.OK {
|
|
||||||
require.Nil(actualStatus)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Equal(tt.expectedStatusCode, actualStatus.Code())
|
|
||||||
require.Contains(actualStatus.Message(), tt.expectedStatusMsg)
|
|
||||||
require.Len(actualStatus.Details(), 0)
|
|
||||||
CheckSimilarPermission(assert, tt.expectedPermission, permission, true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_BadRequestStatusf(t *testing.T) {
|
|
||||||
require, assert := require.New(t), assert.New(t)
|
|
||||||
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_BadRequestStatus(t *testing.T) {
|
|
||||||
require, assert := require.New(t), assert.New(t)
|
|
||||||
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)
|
|
||||||
require.True(ok)
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package errors
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"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"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestAwsHttpResponseError returns an aws http response error
|
|
||||||
func TestAwsHttpResponseError(code int, msg string) error {
|
|
||||||
return &awshttp.ResponseError{
|
|
||||||
ResponseError: &smithyhttp.ResponseError{
|
|
||||||
Response: &smithyhttp.Response{
|
|
||||||
Response: &http.Response{
|
|
||||||
StatusCode: code,
|
|
||||||
Body: io.NopCloser(strings.NewReader(msg)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAwsError returns an generic api error
|
|
||||||
func TestAwsError(code, msg string) error {
|
|
||||||
return &smithy.GenericAPIError{
|
|
||||||
Code: code,
|
|
||||||
Message: msg,
|
|
||||||
Fault: smithy.FaultServer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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{
|
|
||||||
State: pb.StateType_STATE_TYPE_OK,
|
|
||||||
CheckedAt: timestamppb.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected != nil {
|
|
||||||
assert.NotNil(actual)
|
|
||||||
if actual != nil {
|
|
||||||
assert.Equal(expected.State, actual.State, "StateType mismatch")
|
|
||||||
assert.Equal(expected.ErrorDetails, actual.ErrorDetails)
|
|
||||||
assert.NotNil(actual.CheckedAt)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assert.Equal(expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package values
|
package values
|
||||||
@@ -84,33 +84,6 @@ func GetTimeValue(in *structpb.Struct, k string) (time.Time, error) {
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMapStringString returns a map[string]string value and no error if the given key
|
|
||||||
// is found in the provided proto struct input. An error is returned if the key
|
|
||||||
// is not found or the value type is not map[string]string.
|
|
||||||
func GetMapStringString(in *structpb.Struct, k string, required bool) (map[string]string, error) {
|
|
||||||
mv := in.GetFields()
|
|
||||||
v, ok := mv[k]
|
|
||||||
if !ok {
|
|
||||||
if required {
|
|
||||||
return nil, fmt.Errorf("missing required value %q", k)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
result := map[string]string{}
|
|
||||||
m, ok := v.AsInterface().(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("unexpected type for value %q: want map[string]string, got %T", k, v.AsInterface())
|
|
||||||
}
|
|
||||||
for mapKey, mapValue := range m {
|
|
||||||
e, ok := mapValue.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("unexpected type for value in map[%q]: want string, got %T", mapKey, mapValue)
|
|
||||||
}
|
|
||||||
result[mapKey] = e
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StructFields returns a map[string]struct{} of the
|
// StructFields returns a map[string]struct{} of the
|
||||||
// proto struct input.
|
// proto struct input.
|
||||||
func StructFields(s *structpb.Struct) map[string]struct{} {
|
func StructFields(s *structpb.Struct) map[string]struct{} {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package values
|
package values
|
||||||
@@ -208,95 +208,3 @@ func TestGetTimeValue(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetMapStringString(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
in *structpb.Struct
|
|
||||||
key string
|
|
||||||
required bool
|
|
||||||
expected map[string]string
|
|
||||||
expectedErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "required missing",
|
|
||||||
in: &structpb.Struct{},
|
|
||||||
key: "foo",
|
|
||||||
required: true,
|
|
||||||
expectedErr: "missing required value \"foo\"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "optional missing",
|
|
||||||
in: &structpb.Struct{},
|
|
||||||
key: "foo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-map[string]string value",
|
|
||||||
in: &structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
"foo": structpb.NewBoolValue(true),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
key: "foo",
|
|
||||||
expectedErr: "unexpected type for value \"foo\": want map[string]string, got bool",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty map",
|
|
||||||
in: &structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
key: "foo",
|
|
||||||
expected: map[string]string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-string value in map",
|
|
||||||
in: &structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
"bar": structpb.NewBoolValue(true),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
key: "foo",
|
|
||||||
expectedErr: "unexpected type for value in map[\"bar\"]: want string, got bool",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "good",
|
|
||||||
in: &structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
"foo": structpb.NewStructValue(&structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
"bar": structpb.NewStringValue("zoo"),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
key: "foo",
|
|
||||||
expected: map[string]string{
|
|
||||||
"bar": "zoo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
actual, err := GetMapStringString(tc.in, tc.key, tc.required)
|
|
||||||
if tc.expectedErr != "" {
|
|
||||||
require.EqualError(err, tc.expectedErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(err)
|
|
||||||
require.Equal(tc.expected, actual)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package plugin
|
package plugin
|
||||||
@@ -32,6 +32,6 @@ type AwsPlugin struct {
|
|||||||
func NewAwsPlugin() *AwsPlugin {
|
func NewAwsPlugin() *AwsPlugin {
|
||||||
return &AwsPlugin{
|
return &AwsPlugin{
|
||||||
HostPlugin: &host.HostPlugin{},
|
HostPlugin: &host.HostPlugin{},
|
||||||
StoragePlugin: storage.New(),
|
StoragePlugin: &storage.StoragePlugin{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,27 +105,18 @@ a configured IAM user for this provider:
|
|||||||
The following `attributes` are valid on an AWS host catalog resource:
|
The following `attributes` are valid on an AWS host catalog resource:
|
||||||
|
|
||||||
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
|
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
|
||||||
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) section
|
||||||
|
above.
|
||||||
- `region` (string): The region to configure the host catalog for. All host sets
|
- `region` (string): The region to configure the host catalog for. All host sets
|
||||||
in this catalog will be configured for this region.
|
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:
|
The following `secrets` are valid on an AWS host catalog resource:
|
||||||
|
|
||||||
- `access_key_id` (string): The access key ID for the IAM user to use with this
|
- `access_key_id` (string): The access key ID for the IAM user to use with this
|
||||||
host catalog.
|
host catalog.
|
||||||
- `secret_access_key` (string): The secret access key for the IAM user to use
|
- `secret_access_key` (string): The secret access key for the IAM user to use
|
||||||
with this host catalog.
|
with this host catalog.
|
||||||
|
|
||||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
|
||||||
|
|
||||||
### Host Set
|
### Host Set
|
||||||
|
|
||||||
The following attributes are valid on an AWS host Set resource:
|
The following attributes are valid on an AWS host Set resource:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package host
|
package host
|
||||||
@@ -19,9 +19,6 @@ import (
|
|||||||
// map.
|
// map.
|
||||||
type CatalogAttributes struct {
|
type CatalogAttributes struct {
|
||||||
*cred.CredentialAttributes
|
*cred.CredentialAttributes
|
||||||
|
|
||||||
// DualStack is used for configuring how the aws client will resolve requests.
|
|
||||||
DualStack bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
||||||
@@ -34,12 +31,6 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
|||||||
return nil, err
|
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 {
|
for s := range unknownFields {
|
||||||
switch s {
|
switch s {
|
||||||
// Ignore knownFields from CredentialAttributes
|
// Ignore knownFields from CredentialAttributes
|
||||||
@@ -47,14 +38,6 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
|||||||
continue
|
continue
|
||||||
case cred.ConstDisableCredentialRotation:
|
case cred.ConstDisableCredentialRotation:
|
||||||
continue
|
continue
|
||||||
case cred.ConstRoleArn:
|
|
||||||
continue
|
|
||||||
case cred.ConstRoleExternalId:
|
|
||||||
continue
|
|
||||||
case cred.ConstRoleSessionName:
|
|
||||||
continue
|
|
||||||
case cred.ConstRoleTags:
|
|
||||||
continue
|
|
||||||
default:
|
default:
|
||||||
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
||||||
}
|
}
|
||||||
@@ -66,7 +49,6 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
|||||||
|
|
||||||
return &CatalogAttributes{
|
return &CatalogAttributes{
|
||||||
CredentialAttributes: credAttributes,
|
CredentialAttributes: credAttributes,
|
||||||
DualStack: dualStack,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package host
|
package host
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||||
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/plugin"
|
"github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -21,50 +20,36 @@ import (
|
|||||||
func TestGetCatalogAttributes(t *testing.T) {
|
func TestGetCatalogAttributes(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
in *structpb.Struct
|
in map[string]any
|
||||||
expected *CatalogAttributes
|
expected *CatalogAttributes
|
||||||
expectedErrContains string
|
expectedErrContains string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "missing region",
|
name: "missing region",
|
||||||
in: &structpb.Struct{
|
in: map[string]any{},
|
||||||
Fields: make(map[string]*structpb.Value),
|
|
||||||
},
|
|
||||||
expectedErrContains: "missing required value \"region\"",
|
expectedErrContains: "missing required value \"region\"",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "bad value for disable_credential_rotation",
|
||||||
|
in: map[string]any{
|
||||||
|
credential.ConstRegion: "us-west-2",
|
||||||
|
credential.ConstDisableCredentialRotation: "sure",
|
||||||
|
},
|
||||||
|
expectedErrContains: "unexpected type for value \"disable_credential_rotation\": want bool, got string",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "unknown fields",
|
name: "unknown fields",
|
||||||
in: &structpb.Struct{
|
in: map[string]any{
|
||||||
Fields: map[string]*structpb.Value{
|
credential.ConstRegion: "us-west-2",
|
||||||
"region": structpb.NewStringValue("us-west-2"),
|
"foo": true,
|
||||||
"foo": structpb.NewBoolValue(true),
|
"bar": true,
|
||||||
"bar": structpb.NewBoolValue(true),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
|
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",
|
name: "default",
|
||||||
in: &structpb.Struct{
|
in: map[string]any{
|
||||||
Fields: map[string]*structpb.Value{
|
credential.ConstRegion: "us-west-2",
|
||||||
"region": structpb.NewStringValue("us-west-2"),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
expected: &CatalogAttributes{
|
expected: &CatalogAttributes{
|
||||||
CredentialAttributes: &credential.CredentialAttributes{
|
CredentialAttributes: &credential.CredentialAttributes{
|
||||||
@@ -74,31 +59,15 @@ func TestGetCatalogAttributes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "credential attributes",
|
name: "with disable_credential_rotation",
|
||||||
in: &structpb.Struct{
|
in: map[string]any{
|
||||||
Fields: map[string]*structpb.Value{
|
credential.ConstRegion: "us-west-2",
|
||||||
"region": structpb.NewStringValue("us-west-2"),
|
credential.ConstDisableCredentialRotation: true,
|
||||||
"disable_credential_rotation": structpb.NewBoolValue(true),
|
|
||||||
"role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
|
|
||||||
"role_external_id": structpb.NewStringValue("1234567890"),
|
|
||||||
"role_session_name": structpb.NewStringValue("test-session"),
|
|
||||||
"role_tags": structpb.NewStructValue(&structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
"foo": structpb.NewStringValue("bar"),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
expected: &CatalogAttributes{
|
expected: &CatalogAttributes{
|
||||||
CredentialAttributes: &cred.CredentialAttributes{
|
CredentialAttributes: &credential.CredentialAttributes{
|
||||||
Region: "us-west-2",
|
Region: "us-west-2",
|
||||||
DisableCredentialRotation: true,
|
DisableCredentialRotation: true,
|
||||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
|
||||||
RoleExternalId: "1234567890",
|
|
||||||
RoleSessionName: "test-session",
|
|
||||||
RoleTags: map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -109,7 +78,10 @@ func TestGetCatalogAttributes(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
actual, err := getCatalogAttributes(tc.in)
|
input, err := structpb.NewStruct(tc.in)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
actual, err := getCatalogAttributes(input)
|
||||||
if tc.expectedErrContains != "" {
|
if tc.expectedErrContains != "" {
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
require.Contains(err.Error(), tc.expectedErrContains)
|
require.Contains(err.Error(), tc.expectedErrContains)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package host
|
package host
|
||||||
@@ -10,7 +10,4 @@ const (
|
|||||||
// ConstInstanceNameTagKey denotes the special AWS tag key that contains an EC2
|
// ConstInstanceNameTagKey denotes the special AWS tag key that contains an EC2
|
||||||
// instance's name. This field is to be matched literally.
|
// instance's name. This field is to be matched literally.
|
||||||
ConstInstanceNameTagKey = "Name"
|
ConstInstanceNameTagKey = "Name"
|
||||||
|
|
||||||
// ConstAwsDualStack is the key for the dualstack flag in the aws s3 client.
|
|
||||||
ConstAwsDualStack = "dual_stack"
|
|
||||||
)
|
)
|
||||||
+236
-255
@@ -1,23 +1,25 @@
|
|||||||
package host
|
package host
|
||||||
|
|
||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
|
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"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
|
// HostPlugin implements the HostPluginServiceServer interface for the
|
||||||
@@ -39,19 +41,24 @@ var _ pb.HostPluginServiceServer = (*HostPlugin)(nil)
|
|||||||
func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
|
func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
|
||||||
catalog := req.GetCatalog()
|
catalog := req.GetCatalog()
|
||||||
if catalog == nil {
|
if catalog == nil {
|
||||||
return nil, errors.BadRequestStatus("catalog is required")
|
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets := catalog.GetSecrets()
|
||||||
|
if secrets == nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "secrets are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs := catalog.GetAttributes()
|
attrs := catalog.GetAttributes()
|
||||||
if attrs == nil {
|
if attrs == nil {
|
||||||
return nil, errors.BadRequestStatus("attributes are required")
|
return nil, status.Error(codes.InvalidArgument, "attributes are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogAttributes, err := getCatalogAttributes(attrs)
|
catalogAttributes, err := getCatalogAttributes(attrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
credConfig, err := credential.GetCredentialsConfig(catalog.GetSecrets(), catalogAttributes.CredentialAttributes, catalogAttributes.DualStack)
|
credConfig, err := cred.GetCredentialsConfig(secrets, catalogAttributes.Region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -59,18 +66,13 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
|||||||
// Set up the creds in our persisted state.
|
// Set up the creds in our persisted state.
|
||||||
credState, err := credential.NewAwsCredentialPersistedState(
|
credState, err := credential.NewAwsCredentialPersistedState(
|
||||||
append([]credential.AwsCredentialPersistedStateOption{
|
append([]credential.AwsCredentialPersistedStateOption{
|
||||||
credential.WithCredentialsConfig(credConfig),
|
credential.WithAccessKeyId(credConfig.AccessKey),
|
||||||
|
credential.WithSecretAccessKey(credConfig.SecretKey),
|
||||||
|
credential.WithRegion(credConfig.Region),
|
||||||
}, p.testCredStateOpts...)...,
|
}, p.testCredStateOpts...)...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "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(
|
catalogState, err := newAwsCatalogPersistedState(
|
||||||
@@ -79,21 +81,24 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
|||||||
}, p.testCatalogStateOpts...)...,
|
}, p.testCatalogStateOpts...)...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// perform dry run to ensure we can interact with AWS as expected.
|
// Try to rotate the credentials if we're not skipping them.
|
||||||
opts := []ec2Option{}
|
if !catalogAttributes.DisableCredentialRotation {
|
||||||
if catalogAttributes.DualStack {
|
if err := credState.RotateCreds(); err != nil {
|
||||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||||
}
|
}
|
||||||
if st := dryRunValidation(ctx, catalogState, opts); st != nil {
|
} else {
|
||||||
return nil, st.Err()
|
// Simply validate if we aren't rotating.
|
||||||
|
if err := credState.ValidateCreds(); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
persistedProto, err := catalogState.toProto()
|
persistedProto, err := catalogState.toProto()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error converting persisted state to proto: %s", err)
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return &pb.OnCreateCatalogResponse{
|
return &pb.OnCreateCatalogResponse{
|
||||||
@@ -106,105 +111,42 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
|||||||
// For host catalogs we only need to really validate what is
|
// For host catalogs we only need to really validate what is
|
||||||
// currently in the new copy of the host catalog data, so skip
|
// currently in the new copy of the host catalog data, so skip
|
||||||
// fetching the new copy to keep things a little less messy.
|
// fetching the new copy to keep things a little less messy.
|
||||||
currentCatalog := req.GetCurrentCatalog()
|
catalog := req.GetNewCatalog()
|
||||||
if currentCatalog == nil {
|
if catalog == nil {
|
||||||
return nil, errors.BadRequestStatus("current catalog is required")
|
return nil, status.Error(codes.InvalidArgument, "new catalog is nil")
|
||||||
}
|
|
||||||
newCatalog := req.GetNewCatalog()
|
|
||||||
if newCatalog == nil {
|
|
||||||
return nil, errors.BadRequestStatus("new catalog is required")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
oldAttrs := currentCatalog.GetAttributes()
|
var updateSecrets bool
|
||||||
if oldAttrs == nil {
|
secrets := catalog.GetSecrets()
|
||||||
return nil, errors.BadRequestStatus("current catalog attributes are required")
|
if secrets != nil {
|
||||||
|
// We will be updating secrets this run, but what exactly that
|
||||||
|
// means will be determined later.
|
||||||
|
updateSecrets = true
|
||||||
}
|
}
|
||||||
oldCatalogAttributes, err := getCatalogAttributes(oldAttrs)
|
|
||||||
if err != nil {
|
attrs := catalog.GetAttributes()
|
||||||
return nil, err
|
if attrs == nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "new catalog missing attributes")
|
||||||
}
|
}
|
||||||
newAttrs := newCatalog.GetAttributes()
|
|
||||||
if newAttrs == nil {
|
catalogAttributes, err := getCatalogAttributes(attrs)
|
||||||
return nil, errors.BadRequestStatus("new catalog attributes are required")
|
|
||||||
}
|
|
||||||
newCatalogAttributes, err := getCatalogAttributes(newAttrs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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(
|
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||||
req.GetPersisted().GetSecrets(),
|
req.GetPersisted().GetSecrets(),
|
||||||
oldCatalogAttributes.CredentialAttributes,
|
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||||
oldCatalogAttributes.DualStack,
|
|
||||||
p.testCredStateOpts...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "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(
|
catalogState, err := newAwsCatalogPersistedState(
|
||||||
@@ -213,21 +155,44 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
|||||||
}, p.testCatalogStateOpts...)...,
|
}, p.testCatalogStateOpts...)...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// perform dry run to ensure we can interact with AWS as expected.
|
if catalogAttributes.DisableCredentialRotation && !updateSecrets {
|
||||||
opts := []ec2Option{}
|
// This is a validate check to make sure that we aren't disabling
|
||||||
if newCatalogAttributes.DualStack {
|
// rotation for credentials currently being managed by rotation.
|
||||||
opts = append(opts, WithDualStack(newCatalogAttributes.DualStack))
|
// This is not allowed.
|
||||||
}
|
if !credState.CredsLastRotatedTime.IsZero() {
|
||||||
if st := dryRunValidation(ctx, catalogState, opts); st != nil {
|
return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
|
||||||
return nil, st.Err()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if updateSecrets {
|
||||||
|
catalogSecrets, err := credential.GetCredentialsConfig(secrets, catalogAttributes.Region)
|
||||||
|
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(catalogSecrets.AccessKey, catalogSecrets.SecretKey); 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(); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// That's it!
|
||||||
persistedProto, err := catalogState.toProto()
|
persistedProto, err := catalogState.toProto()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error converting persisted state to proto: %s", err)
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return &pb.OnUpdateCatalogResponse{
|
return &pb.OnUpdateCatalogResponse{
|
||||||
@@ -239,12 +204,12 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
|||||||
func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalogRequest) (*pb.OnDeleteCatalogResponse, error) {
|
func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalogRequest) (*pb.OnDeleteCatalogResponse, error) {
|
||||||
catalog := req.GetCatalog()
|
catalog := req.GetCatalog()
|
||||||
if catalog == nil {
|
if catalog == nil {
|
||||||
return nil, errors.BadRequestStatus("catalog is required")
|
return nil, status.Error(codes.InvalidArgument, "new catalog is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs := catalog.GetAttributes()
|
attrs := catalog.GetAttributes()
|
||||||
if attrs == nil {
|
if attrs == nil {
|
||||||
return nil, errors.BadRequestStatus("catalog attributes are required")
|
return nil, status.Error(codes.InvalidArgument, "new catalog missing attributes")
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogAttributes, err := getCatalogAttributes(attrs)
|
catalogAttributes, err := getCatalogAttributes(attrs)
|
||||||
@@ -260,11 +225,9 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
|
|||||||
// may be corrupt/and or legitimately missing.
|
// may be corrupt/and or legitimately missing.
|
||||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||||
req.GetPersisted().GetSecrets(),
|
req.GetPersisted().GetSecrets(),
|
||||||
catalogAttributes.CredentialAttributes,
|
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||||
catalogAttributes.DualStack,
|
|
||||||
p.testCredStateOpts...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = newAwsCatalogPersistedState(
|
_, err = newAwsCatalogPersistedState(
|
||||||
@@ -273,18 +236,15 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo
|
|||||||
}, p.testCatalogStateOpts...)...,
|
}, p.testCatalogStateOpts...)...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to delete static credentials
|
if !credState.CredsLastRotatedTime.IsZero() {
|
||||||
if credential.GetCredentialType(credState.CredentialsConfig) == credential.StaticAWS {
|
// Delete old/existing credentials. This is done with the same
|
||||||
if !credState.CredsLastRotatedTime.IsZero() {
|
// credentials to ensure that it has the proper permissions to do
|
||||||
// Delete old/existing credentials. This is done with the same
|
// it.
|
||||||
// credentials to ensure that it has the proper permissions to do
|
if err := credState.DeleteCreds(); err != nil {
|
||||||
// it.
|
return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
|
||||||
if err := credState.DeleteCreds(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,12 +282,12 @@ func (p *HostPlugin) NormalizeSetData(ctx context.Context, req *pb.NormalizeSetD
|
|||||||
func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) {
|
func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) {
|
||||||
catalog := req.GetCatalog()
|
catalog := req.GetCatalog()
|
||||||
if catalog == nil {
|
if catalog == nil {
|
||||||
return nil, errors.BadRequestStatus("catalog is required")
|
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogAttrsRaw := catalog.GetAttributes()
|
catalogAttrsRaw := catalog.GetAttributes()
|
||||||
if catalogAttrsRaw == nil {
|
if catalogAttrsRaw == nil {
|
||||||
return nil, errors.BadRequestStatus("catalog attributes are required")
|
return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
||||||
@@ -337,11 +297,9 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
|||||||
|
|
||||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||||
req.GetPersisted().GetSecrets(),
|
req.GetPersisted().GetSecrets(),
|
||||||
catalogAttributes.CredentialAttributes,
|
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||||
catalogAttributes.DualStack,
|
|
||||||
p.testCredStateOpts...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogState, err := newAwsCatalogPersistedState(
|
catalogState, err := newAwsCatalogPersistedState(
|
||||||
@@ -350,47 +308,55 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
|||||||
}, p.testCatalogStateOpts...)...,
|
}, p.testCatalogStateOpts...)...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
set := req.GetSet()
|
set := req.GetSet()
|
||||||
if set == nil {
|
if set == nil {
|
||||||
return nil, errors.BadRequestStatus("set is required")
|
return nil, status.Error(codes.InvalidArgument, "set is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if set.GetAttributes() == nil {
|
if set.GetAttributes() == nil {
|
||||||
return nil, errors.BadRequestStatus("set attributes are required")
|
return nil, status.Error(codes.InvalidArgument, "set missing attributes")
|
||||||
}
|
}
|
||||||
setAttrs, err := getSetAttributes(set.GetAttributes())
|
setAttrs, err := getSetAttributes(set.GetAttributes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
describeInstanceFilters, err := buildFilters(setAttrs)
|
ec2Client, err := catalogState.EC2Client(catalogAttributes.Region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error building set filters: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := []ec2Option{}
|
input, err := buildDescribeInstancesInput(setAttrs, true)
|
||||||
if catalogAttributes.DualStack {
|
if err != nil {
|
||||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
|
||||||
}
|
}
|
||||||
if st := dryRunValidation(ctx, catalogState, opts, describeInstanceFilters...); st != nil {
|
|
||||||
return nil, st.Err()
|
_, err = ec2Client.DescribeInstances(input)
|
||||||
|
if err == nil {
|
||||||
|
return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found")
|
||||||
}
|
}
|
||||||
return &pb.OnCreateSetResponse{}, nil
|
|
||||||
|
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" {
|
||||||
|
// Success
|
||||||
|
return &pb.OnCreateSetResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "error performing dry run of DescribeInstances: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnUpdateSet is called when a dynamic host set is updated.
|
// OnUpdateSet is called when a dynamic host set is updated.
|
||||||
func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) {
|
func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) {
|
||||||
catalog := req.GetCatalog()
|
catalog := req.GetCatalog()
|
||||||
if catalog == nil {
|
if catalog == nil {
|
||||||
return nil, errors.BadRequestStatus("catalog is required")
|
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogAttrsRaw := catalog.GetAttributes()
|
catalogAttrsRaw := catalog.GetAttributes()
|
||||||
if catalogAttrsRaw == nil {
|
if catalogAttrsRaw == nil {
|
||||||
return nil, errors.BadRequestStatus("catalog attributes are required")
|
return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
||||||
@@ -400,11 +366,9 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
|||||||
|
|
||||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
||||||
req.GetPersisted().GetSecrets(),
|
req.GetPersisted().GetSecrets(),
|
||||||
catalogAttributes.CredentialAttributes,
|
append(p.testCredStateOpts, credential.WithRegion(catalogAttributes.Region))...)
|
||||||
catalogAttributes.DualStack,
|
|
||||||
p.testCredStateOpts...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogState, err := newAwsCatalogPersistedState(
|
catalogState, err := newAwsCatalogPersistedState(
|
||||||
@@ -413,38 +377,45 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
|||||||
}, p.testCatalogStateOpts...)...,
|
}, p.testCatalogStateOpts...)...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// As with catalog, we don't need to really look at the old host
|
// 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 here, just need to work off of/validate the new set config
|
||||||
set := req.GetNewSet()
|
set := req.GetNewSet()
|
||||||
if set == nil {
|
if set == nil {
|
||||||
return nil, errors.BadRequestStatus("new set is required")
|
return nil, status.Error(codes.InvalidArgument, "new set is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if set.GetAttributes() == nil {
|
if set.GetAttributes() == nil {
|
||||||
return nil, errors.BadRequestStatus("new set attributes are required")
|
return nil, status.Error(codes.InvalidArgument, "new set missing attributes")
|
||||||
}
|
}
|
||||||
setAttrs, err := getSetAttributes(set.GetAttributes())
|
setAttrs, err := getSetAttributes(set.GetAttributes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
describeInstanceFilters, err := buildFilters(setAttrs)
|
ec2Client, err := catalogState.EC2Client(catalogAttributes.Region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error building set filters: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := []ec2Option{}
|
input, err := buildDescribeInstancesInput(setAttrs, true)
|
||||||
if catalogAttributes.DualStack {
|
if err != nil {
|
||||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
|
||||||
}
|
|
||||||
if st := dryRunValidation(ctx, catalogState, opts, describeInstanceFilters...); st != nil {
|
|
||||||
return nil, st.Err()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &pb.OnUpdateSetResponse{}, nil
|
_, err = ec2Client.DescribeInstances(input)
|
||||||
|
if err == nil {
|
||||||
|
return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" {
|
||||||
|
// Success
|
||||||
|
return &pb.OnUpdateSetResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "error performing dry run of DescribeInstances: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnDeleteSet is called when a dynamic host set is deleted.
|
// OnDeleteSet is called when a dynamic host set is deleted.
|
||||||
@@ -458,12 +429,12 @@ func (p *HostPlugin) OnDeleteSet(ctx context.Context, req *pb.OnDeleteSetRequest
|
|||||||
func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) {
|
func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) {
|
||||||
catalog := req.GetCatalog()
|
catalog := req.GetCatalog()
|
||||||
if catalog == nil {
|
if catalog == nil {
|
||||||
return nil, errors.BadRequestStatus("catalog is required")
|
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogAttrsRaw := catalog.GetAttributes()
|
catalogAttrsRaw := catalog.GetAttributes()
|
||||||
if catalogAttrsRaw == nil {
|
if catalogAttrsRaw == nil {
|
||||||
return nil, errors.BadRequestStatus("catalog attributes are required")
|
return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
||||||
@@ -471,13 +442,9 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
credState, err := credential.AwsCredentialPersistedStateFromProto(req.GetPersisted().GetSecrets(), p.testCredStateOpts...)
|
||||||
req.GetPersisted().GetSecrets(),
|
|
||||||
catalogAttributes.CredentialAttributes,
|
|
||||||
catalogAttributes.DualStack,
|
|
||||||
p.testCredStateOpts...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogState, err := newAwsCatalogPersistedState(
|
catalogState, err := newAwsCatalogPersistedState(
|
||||||
@@ -486,12 +453,12 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
|||||||
}, p.testCatalogStateOpts...)...,
|
}, p.testCatalogStateOpts...)...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sets := req.GetSets()
|
sets := req.GetSets()
|
||||||
if sets == nil {
|
if sets == nil {
|
||||||
return nil, errors.BadRequestStatus("sets are required")
|
return nil, status.Error(codes.InvalidArgument, "sets is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build all the queries in advance.
|
// Build all the queries in advance.
|
||||||
@@ -506,11 +473,11 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
|||||||
for i, set := range sets {
|
for i, set := range sets {
|
||||||
// Validate Id since we use it in output
|
// Validate Id since we use it in output
|
||||||
if set.GetId() == "" {
|
if set.GetId() == "" {
|
||||||
return nil, errors.BadRequestStatus("set id is required")
|
return nil, status.Error(codes.InvalidArgument, "set missing id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if set.GetAttributes() == nil {
|
if set.GetAttributes() == nil {
|
||||||
return nil, errors.BadRequestStatusf("set %s attributes are required", set.GetId())
|
return nil, status.Error(codes.InvalidArgument, "set missing attributes")
|
||||||
}
|
}
|
||||||
setAttrs, err := getSetAttributes(set.GetAttributes())
|
setAttrs, err := getSetAttributes(set.GetAttributes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -519,7 +486,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
|||||||
|
|
||||||
input, err := buildDescribeInstancesInput(setAttrs, false)
|
input, err := buildDescribeInstancesInput(setAttrs, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error building DescribeInstances parameters for host set id %q: %s", set.GetId(), err)
|
return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters for host set id %q: %s", set.GetId(), err)
|
||||||
}
|
}
|
||||||
queries[i] = hostSetQuery{
|
queries[i] = hostSetQuery{
|
||||||
Id: set.GetId(),
|
Id: set.GetId(),
|
||||||
@@ -527,21 +494,17 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := []ec2Option{}
|
ec2Client, err := catalogState.EC2Client(catalogAttributes.Region)
|
||||||
if catalogAttributes.DualStack {
|
|
||||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
|
||||||
}
|
|
||||||
ec2Client, err := catalogState.EC2Client(ctx, opts...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error getting EC2 client: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run all queries now and assemble output.
|
// Run all queries now and assemble output.
|
||||||
var maxLen int
|
var maxLen int
|
||||||
for i, query := range queries {
|
for i, query := range queries {
|
||||||
output, err := ec2Client.DescribeInstances(ctx, query.Input)
|
output, err := ec2Client.DescribeInstances(query.Input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error running DescribeInstances for host set id %q: %s", query.Id, err)
|
return nil, status.Errorf(codes.InvalidArgument, "error running DescribeInstances for host set id %q: %s", query.Id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
queries[i].Output = output
|
queries[i].Output = output
|
||||||
@@ -552,7 +515,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
|||||||
for _, instance := range reservation.Instances {
|
for _, instance := range reservation.Instances {
|
||||||
host, err := awsInstanceToHost(instance)
|
host, err := awsInstanceToHost(instance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error processing host results for host set id %q: %s", query.Id, err)
|
return nil, status.Errorf(codes.InvalidArgument, "error processing host results for host set id %q: %s", query.Id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
queries[i].OutputHosts = append(queries[i].OutputHosts, host)
|
queries[i].OutputHosts = append(queries[i].OutputHosts, host)
|
||||||
@@ -593,8 +556,8 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFilters(attrs *SetAttributes) ([]types.Filter, error) {
|
func buildFilters(attrs *SetAttributes) ([]*ec2.Filter, error) {
|
||||||
var filters []types.Filter
|
var filters []*ec2.Filter
|
||||||
var foundStateFilter bool
|
var foundStateFilter bool
|
||||||
for _, filterAttr := range attrs.Filters {
|
for _, filterAttr := range attrs.Filters {
|
||||||
// Each filter arrives in "k=v1,v2" format. First, split on equal sign.
|
// Each filter arrives in "k=v1,v2" format. First, split on equal sign.
|
||||||
@@ -613,9 +576,14 @@ func buildFilters(attrs *SetAttributes) ([]types.Filter, error) {
|
|||||||
foundStateFilter = true
|
foundStateFilter = true
|
||||||
}
|
}
|
||||||
|
|
||||||
filters = append(filters, types.Filter{
|
var filterValues []*string
|
||||||
|
for _, val := range strings.Split(filterValue, ",") {
|
||||||
|
filterValues = append(filterValues, aws.String(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
filters = append(filters, &ec2.Filter{
|
||||||
Name: aws.String(filterKey),
|
Name: aws.String(filterKey),
|
||||||
Values: strings.Split(filterValue, ","),
|
Values: filterValues,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,9 +592,9 @@ func buildFilters(attrs *SetAttributes) ([]types.Filter, error) {
|
|||||||
// instance-state-name = ["running"] to the filter set. This
|
// instance-state-name = ["running"] to the filter set. This
|
||||||
// ensures that we filter on running instances only at the API
|
// ensures that we filter on running instances only at the API
|
||||||
// side, saving time when processing results.
|
// side, saving time when processing results.
|
||||||
filters = append(filters, types.Filter{
|
filters = append(filters, &ec2.Filter{
|
||||||
Name: aws.String("instance-state-name"),
|
Name: aws.String("instance-state-name"),
|
||||||
Values: []string{string(types.InstanceStateNameRunning)},
|
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,49 +616,97 @@ func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.Descri
|
|||||||
// awsInstanceToHost processes data from an ec2.Instance and returns
|
// awsInstanceToHost processes data from an ec2.Instance and returns
|
||||||
// a ListHostsResponseHost with the ID and network addresses
|
// a ListHostsResponseHost with the ID and network addresses
|
||||||
// populated.
|
// populated.
|
||||||
func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, error) {
|
func awsInstanceToHost(instance *ec2.Instance) (*pb.ListHostsResponseHost, error) {
|
||||||
if aws.ToString(instance.InstanceId) == "" {
|
// Integrity check: some fields should always be non-nil. Check
|
||||||
return nil, fmt.Errorf("response integrity error: missing instance id")
|
// those.
|
||||||
|
if instance == nil {
|
||||||
|
return nil, errors.New("response integrity error: missing instance entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if aws.StringValue(instance.InstanceId) == "" {
|
||||||
|
return nil, errors.New("response integrity error: missing instance id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result := new(pb.ListHostsResponseHost)
|
result := new(pb.ListHostsResponseHost)
|
||||||
|
|
||||||
// External ID is the instance ID.
|
// External ID is the instance ID.
|
||||||
result.ExternalId = aws.ToString(instance.InstanceId)
|
result.ExternalId = aws.StringValue(instance.InstanceId)
|
||||||
|
|
||||||
// External Name is the AWS "Name" tag, if it exists.
|
// External Name is the AWS "Name" tag, if it exists.
|
||||||
for _, t := range instance.Tags {
|
for _, t := range instance.Tags {
|
||||||
if aws.ToString(t.Key) == ConstInstanceNameTagKey {
|
if aws.StringValue(t.Key) == ConstInstanceNameTagKey {
|
||||||
result.ExternalName = aws.ToString(t.Value)
|
result.ExternalName = aws.StringValue(t.Value)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.IpAddresses = appendDistinct(result.IpAddresses, instance.PrivateIpAddress, instance.PublicIpAddress)
|
// First IP address/dns name are always the private fields if they
|
||||||
result.DnsNames = appendDistinct(result.DnsNames, instance.PrivateDnsName, instance.PublicDnsName)
|
// are populated
|
||||||
|
if aws.StringValue(instance.PrivateIpAddress) != "" {
|
||||||
|
result.IpAddresses = append(result.IpAddresses, aws.StringValue(instance.PrivateIpAddress))
|
||||||
|
}
|
||||||
|
if aws.StringValue(instance.PrivateDnsName) != "" {
|
||||||
|
result.DnsNames = append(result.DnsNames, aws.StringValue(instance.PrivateDnsName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public IP address/dns names are next
|
||||||
|
if aws.StringValue(instance.PublicIpAddress) != "" {
|
||||||
|
result.IpAddresses = append(result.IpAddresses, aws.StringValue(instance.PublicIpAddress))
|
||||||
|
}
|
||||||
|
if aws.StringValue(instance.PublicDnsName) != "" {
|
||||||
|
result.DnsNames = append(result.DnsNames, aws.StringValue(instance.PublicDnsName))
|
||||||
|
}
|
||||||
|
|
||||||
// Now go through all of the interfaces and log the IP address of
|
// Now go through all of the interfaces and log the IP address of
|
||||||
// every interface.
|
// every interface.
|
||||||
for _, iface := range instance.NetworkInterfaces {
|
for _, iface := range instance.NetworkInterfaces {
|
||||||
|
if iface == nil {
|
||||||
|
// Probably will never happen, but just in case
|
||||||
|
return nil, errors.New("response integrity error: interface entry is nil")
|
||||||
|
}
|
||||||
|
|
||||||
// Populate default IP addresses/DNS name similar to how we do
|
// Populate default IP addresses/DNS name similar to how we do
|
||||||
// for the entire instance.
|
// for the entire instance.
|
||||||
result.IpAddresses = appendDistinct(result.IpAddresses, iface.PrivateIpAddress)
|
if aws.StringValue(iface.PrivateIpAddress) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(iface.PrivateIpAddress)) {
|
||||||
result.DnsNames = appendDistinct(result.DnsNames, iface.PrivateDnsName)
|
result.IpAddresses = append(result.IpAddresses, aws.StringValue(iface.PrivateIpAddress))
|
||||||
|
}
|
||||||
|
if aws.StringValue(iface.PrivateDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(iface.PrivateDnsName)) {
|
||||||
|
result.DnsNames = append(result.DnsNames, aws.StringValue(iface.PrivateDnsName))
|
||||||
|
}
|
||||||
|
|
||||||
// Iterate through the private IP addresses and log the
|
// Iterate through the private IP addresses and log the
|
||||||
// information.
|
// information.
|
||||||
for _, addr := range iface.PrivateIpAddresses {
|
for _, addr := range iface.PrivateIpAddresses {
|
||||||
result.IpAddresses = appendDistinct(result.IpAddresses, addr.PrivateIpAddress)
|
if addr == nil {
|
||||||
result.DnsNames = appendDistinct(result.DnsNames, addr.PrivateDnsName)
|
return nil, errors.New("response integrity error: interface address entry is nil")
|
||||||
if addr.Association != nil {
|
}
|
||||||
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Association.PublicIp)
|
|
||||||
result.DnsNames = appendDistinct(result.DnsNames, addr.Association.PublicDnsName)
|
// Add private address/dns name if they have not been added yet
|
||||||
|
if aws.StringValue(addr.PrivateIpAddress) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(addr.PrivateIpAddress)) {
|
||||||
|
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.PrivateIpAddress))
|
||||||
|
}
|
||||||
|
if aws.StringValue(addr.PrivateDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(addr.PrivateDnsName)) {
|
||||||
|
result.DnsNames = append(result.DnsNames, aws.StringValue(addr.PrivateDnsName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add public address/dns name if they have not been added yet
|
||||||
|
if addr.Association != nil && aws.StringValue(addr.Association.PublicIp) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(addr.Association.PublicIp)) {
|
||||||
|
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.Association.PublicIp))
|
||||||
|
}
|
||||||
|
if addr.Association != nil && aws.StringValue(addr.Association.PublicDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(addr.Association.PublicDnsName)) {
|
||||||
|
result.DnsNames = append(result.DnsNames, aws.StringValue(addr.Association.PublicDnsName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the IPv6 addresses.
|
// Add the IPv6 addresses.
|
||||||
for _, addr := range iface.Ipv6Addresses {
|
for _, addr := range iface.Ipv6Addresses {
|
||||||
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Ipv6Address)
|
if addr == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr.Ipv6Address != nil && aws.StringValue(addr.Ipv6Address) != "" {
|
||||||
|
result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.Ipv6Address))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,47 +714,12 @@ func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, erro
|
|||||||
return result, nil
|
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 {
|
|
||||||
for _, e := range elems {
|
|
||||||
value := aws.ToString(e)
|
|
||||||
if value == "" || stringInSlice(slice, value) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
slice = append(slice, value)
|
|
||||||
}
|
|
||||||
return slice
|
|
||||||
}
|
|
||||||
|
|
||||||
// stringInSlice returns true if the slice contains the given element.
|
|
||||||
func stringInSlice(s []string, x string) bool {
|
func stringInSlice(s []string, x string) bool {
|
||||||
for _, y := range s {
|
for _, y := range s {
|
||||||
if x == y {
|
if x == y {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
+449
-1136
File diff suppressed because it is too large.
Load diff
@@ -1,25 +1,39 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package host
|
package host
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
smithyEndpoints "github.com/aws/smithy-go/endpoints"
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EC2API interface {
|
var customClient = &http.Client{
|
||||||
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
DualStack: true,
|
||||||
|
}).DialContext,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ec2APIFunc func(*session.Session, ...*aws.Config) (ec2iface.EC2API, error)
|
||||||
|
|
||||||
|
type awsCatalogPersistedStateOption func(s *awsCatalogPersistedState) error
|
||||||
|
|
||||||
type awsCatalogPersistedState struct {
|
type awsCatalogPersistedState struct {
|
||||||
*credential.AwsCredentialPersistedState
|
*credential.AwsCredentialPersistedState
|
||||||
|
|
||||||
@@ -27,10 +41,6 @@ type awsCatalogPersistedState struct {
|
|||||||
testEC2APIFunc ec2APIFunc
|
testEC2APIFunc ec2APIFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
type ec2APIFunc func(...aws.Config) (EC2API, error)
|
|
||||||
|
|
||||||
type awsCatalogPersistedStateOption func(s *awsCatalogPersistedState) error
|
|
||||||
|
|
||||||
func withTestEC2APIFunc(f ec2APIFunc) awsCatalogPersistedStateOption {
|
func withTestEC2APIFunc(f ec2APIFunc) awsCatalogPersistedStateOption {
|
||||||
return func(s *awsCatalogPersistedState) error {
|
return func(s *awsCatalogPersistedState) error {
|
||||||
if s.testEC2APIFunc != nil {
|
if s.testEC2APIFunc != nil {
|
||||||
@@ -73,70 +83,22 @@ func (s *awsCatalogPersistedState) toProto() (*pb.HostCatalogPersisted, error) {
|
|||||||
|
|
||||||
// EC2Client returns a configured EC2 client based on the session
|
// EC2Client returns a configured EC2 client based on the session
|
||||||
// information stored in the state.
|
// information stored in the state.
|
||||||
func (s *awsCatalogPersistedState) EC2Client(ctx context.Context, opt ...ec2Option) (EC2API, error) {
|
func (s *awsCatalogPersistedState) EC2Client(region string) (ec2iface.EC2API, error) {
|
||||||
opts, err := getOpts(opt...)
|
sess, err := s.GetSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing options when fetching EC2 client: %w", err)
|
return nil, fmt.Errorf("error getting AWS session 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)
|
|
||||||
}
|
|
||||||
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 {
|
if s.testEC2APIFunc != nil {
|
||||||
return s.testEC2APIFunc(*awsCfg)
|
return s.testEC2APIFunc(sess, aws.NewConfig().WithRegion(region))
|
||||||
}
|
}
|
||||||
return ec2.NewFromConfig(*awsCfg, ec2Opts...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getOpts iterates the inbound ec2Options and returns a struct
|
cfg := aws.NewConfig()
|
||||||
func getOpts(opt ...ec2Option) (ec2Options, error) {
|
if region != "" {
|
||||||
opts := getDefaultEc2Options()
|
cfg = cfg.WithRegion(region)
|
||||||
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
|
cfg.HTTPClient = customClient
|
||||||
type ec2Option func(*ec2Options) error
|
|
||||||
|
|
||||||
// options = how options are represented
|
return ec2.New(sess, cfg), nil
|
||||||
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,25 +1,21 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package host
|
package host
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
|
func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
cred, err := credential.NewAwsCredentialPersistedState(
|
cred, err := credential.NewAwsCredentialPersistedState(
|
||||||
credential.WithCredentialsConfig(&awsutil.CredentialsConfig{
|
credential.WithRegion("us-west-2"),
|
||||||
Region: "us-west-2",
|
credential.WithAccessKeyId("foobar"),
|
||||||
AccessKey: "foobar",
|
credential.WithSecretAccessKey("barfoo"),
|
||||||
SecretKey: "barfoo",
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
@@ -29,7 +25,7 @@ func TestAwsCatalogPersistedStateEC2Client(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
clientRaw, err := state.EC2Client(context.Background())
|
clientRaw, err := state.EC2Client("us-west-2")
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.NotNil(clientRaw)
|
require.NotNil(clientRaw)
|
||||||
client, ok := clientRaw.(*testMockEC2)
|
client, ok := clientRaw.(*testMockEC2)
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package host
|
package host
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -30,11 +29,10 @@ func (s *testMockEC2State) Reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type testMockEC2 struct {
|
type testMockEC2 struct {
|
||||||
EC2API
|
ec2iface.EC2API
|
||||||
|
|
||||||
State *testMockEC2State
|
|
||||||
Region string
|
|
||||||
|
|
||||||
|
State *testMockEC2State
|
||||||
|
Region string
|
||||||
DescribeInstancesOutput *ec2.DescribeInstancesOutput
|
DescribeInstancesOutput *ec2.DescribeInstancesOutput
|
||||||
DescribeInstancesError error
|
DescribeInstancesError error
|
||||||
}
|
}
|
||||||
@@ -56,7 +54,7 @@ func testMockEC2WithDescribeInstancesError(e error) testMockEC2Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFunc {
|
func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFunc {
|
||||||
return func(cfgs ...aws.Config) (EC2API, error) {
|
return func(sess *session.Session, cfgs ...*aws.Config) (ec2iface.EC2API, error) {
|
||||||
m := &testMockEC2{
|
m := &testMockEC2{
|
||||||
State: state,
|
State: state,
|
||||||
}
|
}
|
||||||
@@ -69,17 +67,14 @@ func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFu
|
|||||||
|
|
||||||
for _, cfg := range cfgs {
|
for _, cfg := range cfgs {
|
||||||
// Last region takes precedence
|
// Last region takes precedence
|
||||||
if cfg.Region != "" {
|
m.Region = *cfg.Region
|
||||||
m.Region = cfg.Region
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *testMockEC2) DescribeInstances(ctx context.Context, input *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
|
func (m *testMockEC2) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
|
||||||
if m.State != nil {
|
if m.State != nil {
|
||||||
m.State.DescribeInstancesCalled = true
|
m.State.DescribeInstancesCalled = true
|
||||||
m.State.DescribeInstancesInputParams = input
|
m.State.DescribeInstancesInputParams = input
|
||||||
@@ -92,7 +87,7 @@ func (m *testMockEC2) DescribeInstances(ctx context.Context, input *ec2.Describe
|
|||||||
return m.DescribeInstancesOutput, nil
|
return m.DescribeInstancesOutput, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ec2FilterSorter []types.Filter
|
type ec2FilterSorter []*ec2.Filter
|
||||||
|
|
||||||
func (s ec2FilterSorter) Len() int { return len(s) }
|
func (s ec2FilterSorter) Len() int { return len(s) }
|
||||||
func (s ec2FilterSorter) Less(i, j int) bool { return *s[i].Name < *s[j].Name }
|
func (s ec2FilterSorter) Less(i, j int) bool { return *s[i].Name < *s[j].Name }
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Example policy:
|
|||||||
```
|
```
|
||||||
|
|
||||||
* `iam:GetUser`, `iam:CreateAccessKey`, and `iam:DeleteAccessKey`, configured to
|
* `iam:GetUser`, `iam:CreateAccessKey`, and `iam:DeleteAccessKey`, configured to
|
||||||
the IAM user to allow static credential rotation. Example policy:
|
the IAM user to allow credential rotation. Example policy:
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
@@ -66,24 +66,18 @@ Example policy:
|
|||||||
The following `attributes` are valid on an AWS storage bucket resource:
|
The following `attributes` are valid on an AWS storage bucket resource:
|
||||||
|
|
||||||
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
|
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
|
||||||
be performed.
|
be performed. See the [Credential Rotation](../../../README.md#credential-rotation) section
|
||||||
|
above.
|
||||||
- `region` (string): The region to configure the storage bucket for.
|
- `region` (string): The region to configure the storage bucket for.
|
||||||
- `endpoint_url` (string): The endpoint to configure the storage.
|
- `endpoint-url` (string): The endpoint to configure the storage.
|
||||||
- `role_arn` (string): The role arn configured for the assume role provider.
|
|
||||||
- `role_external_id` (string): The external id configured for the assume role provider.
|
|
||||||
- `role_session_name` (string): The session name configured for the assume role provider.
|
|
||||||
- `role_tags` (object): The key-value pair tags configured for the assume role provider.
|
|
||||||
|
|
||||||
An example of how to utilize the [endpoint attribute](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html)
|
An example of how to utilize the [endpoint attribute](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html)
|
||||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
|
||||||
|
|
||||||
### Secrets
|
### Secrets
|
||||||
|
|
||||||
The following `secrets` are optional on an AWS storage bucket resource:
|
The following `secrets` are valid on an AWS storage bucket resource:
|
||||||
|
|
||||||
- `access_key_id` (string): The access key ID for the IAM user to use with this
|
- `access_key_id` (string): The access key ID for the IAM user to use with this
|
||||||
storage bucket.
|
storage bucket.
|
||||||
- `secret_access_key` (string): The secret access key for the IAM user to use
|
- `secret_access_key` (string): The secret access key for the IAM user to use
|
||||||
with this storage bucket.
|
with this storage bucket.
|
||||||
|
|
||||||
See the [Credential Rotation](../../../README.md#credential-rotation) readme for more information.
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
@@ -19,9 +19,6 @@ type StorageAttributes struct {
|
|||||||
|
|
||||||
// EndpointUrl is used for configuring how the aws client will resolve requests.
|
// EndpointUrl is used for configuring how the aws client will resolve requests.
|
||||||
EndpointUrl string
|
EndpointUrl string
|
||||||
|
|
||||||
// DualStack is used for configuring how the aws client will resolve requests.
|
|
||||||
DualStack bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
||||||
@@ -40,12 +37,6 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
|||||||
}
|
}
|
||||||
delete(unknownFields, ConstAwsEndpointUrl)
|
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 {
|
for s := range unknownFields {
|
||||||
switch s {
|
switch s {
|
||||||
// Ignore knownFields from CredentialAttributes
|
// Ignore knownFields from CredentialAttributes
|
||||||
@@ -53,14 +44,6 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
|||||||
continue
|
continue
|
||||||
case cred.ConstDisableCredentialRotation:
|
case cred.ConstDisableCredentialRotation:
|
||||||
continue
|
continue
|
||||||
case cred.ConstRoleArn:
|
|
||||||
continue
|
|
||||||
case cred.ConstRoleExternalId:
|
|
||||||
continue
|
|
||||||
case cred.ConstRoleSessionName:
|
|
||||||
continue
|
|
||||||
case cred.ConstRoleTags:
|
|
||||||
continue
|
|
||||||
default:
|
default:
|
||||||
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
||||||
}
|
}
|
||||||
@@ -73,6 +56,5 @@ func getStorageAttributes(in *structpb.Struct) (*StorageAttributes, error) {
|
|||||||
return &StorageAttributes{
|
return &StorageAttributes{
|
||||||
CredentialAttributes: credAttributes,
|
CredentialAttributes: credAttributes,
|
||||||
EndpointUrl: endpointUrl,
|
EndpointUrl: endpointUrl,
|
||||||
DualStack: dualStack,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
@@ -7,8 +7,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
cred "github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
@@ -18,51 +16,29 @@ import (
|
|||||||
func TestGetStorageAttributes(t *testing.T) {
|
func TestGetStorageAttributes(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
in *structpb.Struct
|
in map[string]any
|
||||||
expected *StorageAttributes
|
expected *StorageAttributes
|
||||||
expectedErrContains string
|
expectedErrContains string
|
||||||
expectedDetails *pb.StorageBucketCredentialState
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "missing region",
|
name: "missing region",
|
||||||
in: &structpb.Struct{
|
in: map[string]any{},
|
||||||
Fields: make(map[string]*structpb.Value),
|
|
||||||
},
|
|
||||||
expectedErrContains: "missing required value \"region\"",
|
expectedErrContains: "missing required value \"region\"",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unknown fields",
|
name: "unknown fields",
|
||||||
in: &structpb.Struct{
|
in: map[string]any{
|
||||||
Fields: map[string]*structpb.Value{
|
"region": "us-west-2",
|
||||||
"region": structpb.NewStringValue("us-west-2"),
|
"foo": true,
|
||||||
"foo": structpb.NewBoolValue(true),
|
"bar": true,
|
||||||
"bar": structpb.NewBoolValue(true),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
|
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",
|
name: "good w/ endpoint",
|
||||||
in: &structpb.Struct{
|
in: map[string]any{
|
||||||
Fields: map[string]*structpb.Value{
|
cred.ConstRegion: "us-west-2",
|
||||||
"region": structpb.NewStringValue("us-west-2"),
|
ConstAwsEndpointUrl: "0.0.0.0",
|
||||||
"endpoint_url": structpb.NewStringValue("0.0.0.0"),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
expected: &StorageAttributes{
|
expected: &StorageAttributes{
|
||||||
CredentialAttributes: &cred.CredentialAttributes{
|
CredentialAttributes: &cred.CredentialAttributes{
|
||||||
@@ -73,10 +49,8 @@ func TestGetStorageAttributes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "good w/o endpoint",
|
name: "good w/o endpoint",
|
||||||
in: &structpb.Struct{
|
in: map[string]any{
|
||||||
Fields: map[string]*structpb.Value{
|
cred.ConstRegion: "us-west-2",
|
||||||
"region": structpb.NewStringValue("us-west-2"),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
expected: &StorageAttributes{
|
expected: &StorageAttributes{
|
||||||
CredentialAttributes: &cred.CredentialAttributes{
|
CredentialAttributes: &cred.CredentialAttributes{
|
||||||
@@ -84,51 +58,21 @@ func TestGetStorageAttributes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "credential attributes",
|
|
||||||
in: &structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
"region": structpb.NewStringValue("us-west-2"),
|
|
||||||
"disable_credential_rotation": structpb.NewBoolValue(true),
|
|
||||||
"role_arn": structpb.NewStringValue("arn:aws:iam::123456789012:role/S3Access"),
|
|
||||||
"role_external_id": structpb.NewStringValue("1234567890"),
|
|
||||||
"role_session_name": structpb.NewStringValue("test-session"),
|
|
||||||
"role_tags": structpb.NewStructValue(&structpb.Struct{
|
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
"foo": structpb.NewStringValue("bar"),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: &StorageAttributes{
|
|
||||||
CredentialAttributes: &cred.CredentialAttributes{
|
|
||||||
Region: "us-west-2",
|
|
||||||
DisableCredentialRotation: true,
|
|
||||||
RoleArn: "arn:aws:iam::123456789012:role/S3Access",
|
|
||||||
RoleExternalId: "1234567890",
|
|
||||||
RoleSessionName: "test-session",
|
|
||||||
RoleTags: map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
require, assert := require.New(t), assert.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
actual, err := getStorageAttributes(tc.in)
|
input, err := structpb.NewStruct(tc.in)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
actual, err := getStorageAttributes(input)
|
||||||
if tc.expectedErrContains != "" {
|
if tc.expectedErrContains != "" {
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
require.Contains(err.Error(), tc.expectedErrContains)
|
require.Contains(err.Error(), tc.expectedErrContains)
|
||||||
require.Equal(codes.InvalidArgument, status.Code(err))
|
require.Equal(status.Code(err), codes.InvalidArgument)
|
||||||
|
|
||||||
st, ok := status.FromError(err)
|
|
||||||
require.True(ok)
|
|
||||||
assert.Len(st.Details(), 0)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
@@ -7,9 +7,6 @@ const (
|
|||||||
// ConstAwsEndpointUrl is the key for the endpoint url in the aws s3 client.
|
// ConstAwsEndpointUrl is the key for the endpoint url in the aws s3 client.
|
||||||
ConstAwsEndpointUrl = "endpoint_url"
|
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 is the recommened chunk size for sending data through a stream
|
||||||
defaultStreamChunkSize = 65536 // 64 KiB
|
defaultStreamChunkSize = 65536 // 64 KiB
|
||||||
)
|
)
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
s3StatusErrMsg = "aws s3 error: %s"
|
|
||||||
|
|
||||||
// s3ErrorBadDigest is returned when there is a checksum
|
|
||||||
// mismatch between the request and the s3 service. This
|
|
||||||
// error code is returned for the PutObject request.
|
|
||||||
s3ErrorBadDigest = "BadDigest"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setStatePermission(req any, permission *pb.Permission) *pb.StorageBucketCredentialState {
|
|
||||||
state := &pb.StorageBucketCredentialState{
|
|
||||||
State: &pb.Permissions{},
|
|
||||||
}
|
|
||||||
switch req.(type) {
|
|
||||||
case pb.HeadObjectRequest:
|
|
||||||
state.State.Read = permission
|
|
||||||
case *pb.HeadObjectRequest:
|
|
||||||
state.State.Read = permission
|
|
||||||
case pb.GetObjectRequest:
|
|
||||||
state.State.Read = permission
|
|
||||||
case *pb.GetObjectRequest:
|
|
||||||
state.State.Read = permission
|
|
||||||
case pb.PutObjectRequest:
|
|
||||||
state.State.Write = permission
|
|
||||||
case *pb.PutObjectRequest:
|
|
||||||
state.State.Write = permission
|
|
||||||
case pb.DeleteObjectsRequest:
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseS3Error will convert an S3 api error into a RPC status. If the
|
|
||||||
// error does not match against an s3 error type, it will fallback to
|
|
||||||
// evaluating against generic aws service errors. This includes throttling,
|
|
||||||
// credentials, and connectivity error types.
|
|
||||||
//
|
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
|
|
||||||
func parseS3Error(op string, err error, req any) *status.Status {
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
st, p := errors.ParseAWSError(op, err)
|
|
||||||
if p != nil {
|
|
||||||
state := setStatePermission(req, p)
|
|
||||||
if st, err = st.WithDetails(state); err != nil {
|
|
||||||
st = status.New(codes.Internal, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return st
|
|
||||||
}
|
|
||||||
|
|
||||||
func checksumMistmatchStatus() error {
|
|
||||||
return status.New(codes.Aborted, "mismatched checksum").Err()
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
|
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_ParseS3Error(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
req any
|
|
||||||
err error
|
|
||||||
expectedStatusCode codes.Code
|
|
||||||
expectedStatusMsg string
|
|
||||||
expectedDetails *pb.StorageBucketCredentialState
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "nil-error",
|
|
||||||
expectedStatusCode: codes.OK,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no-such-bucket",
|
|
||||||
req: &pb.GetObjectRequest{},
|
|
||||||
err: TestAwsS3Error("NoSuchBucket", "GetObject", "no such bucket"),
|
|
||||||
expectedStatusCode: codes.NotFound,
|
|
||||||
expectedStatusMsg: "aws service s3: test:",
|
|
||||||
expectedDetails: &pb.StorageBucketCredentialState{
|
|
||||||
State: &pb.Permissions{
|
|
||||||
Read: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_ERROR,
|
|
||||||
ErrorDetails: "no such bucket",
|
|
||||||
CheckedAt: timestamppb.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no-such-key",
|
|
||||||
req: &pb.GetObjectRequest{},
|
|
||||||
err: TestAwsS3Error("NoSuchKey", "GetObject", "no such key"),
|
|
||||||
expectedStatusCode: codes.NotFound,
|
|
||||||
expectedStatusMsg: "aws service s3: test:",
|
|
||||||
expectedDetails: &pb.StorageBucketCredentialState{
|
|
||||||
State: &pb.Permissions{
|
|
||||||
Read: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
CheckedAt: timestamppb.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "not-found",
|
|
||||||
req: &pb.GetObjectRequest{},
|
|
||||||
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: test:",
|
|
||||||
expectedDetails: &pb.StorageBucketCredentialState{
|
|
||||||
State: &pb.Permissions{
|
|
||||||
Read: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
CheckedAt: timestamppb.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid-object-state",
|
|
||||||
req: &pb.GetObjectRequest{},
|
|
||||||
err: TestAwsS3Error("InvalidObjectState", "GetObject", "invalid object state"),
|
|
||||||
expectedStatusCode: codes.NotFound,
|
|
||||||
expectedStatusMsg: "aws service s3: test:",
|
|
||||||
expectedDetails: &pb.StorageBucketCredentialState{
|
|
||||||
State: &pb.Permissions{
|
|
||||||
Read: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
CheckedAt: timestamppb.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad-digest",
|
|
||||||
req: &pb.PutObjectRequest{},
|
|
||||||
err: TestAwsS3Error(s3ErrorBadDigest, "PutObject", "checksum mismatch"),
|
|
||||||
expectedStatusCode: codes.Aborted,
|
|
||||||
expectedStatusMsg: "aws service s3: test:",
|
|
||||||
expectedDetails: &pb.StorageBucketCredentialState{
|
|
||||||
State: &pb.Permissions{
|
|
||||||
Write: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
CheckedAt: timestamppb.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fallback-throttle-error",
|
|
||||||
req: &pb.GetObjectRequest{},
|
|
||||||
err: TestAwsS3Error("Throttling", "action", "throttling exception"),
|
|
||||||
expectedStatusCode: codes.Unavailable,
|
|
||||||
expectedStatusMsg: "aws service s3: throttling: test:",
|
|
||||||
expectedDetails: &pb.StorageBucketCredentialState{
|
|
||||||
State: &pb.Permissions{
|
|
||||||
Read: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
CheckedAt: timestamppb.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fallback-connectivity-error",
|
|
||||||
req: &pb.GetObjectRequest{},
|
|
||||||
err: TestAwsS3Error("RequestTimeout", "action", "request timeout"),
|
|
||||||
expectedStatusCode: codes.DeadlineExceeded,
|
|
||||||
expectedStatusMsg: "aws service s3: test:",
|
|
||||||
expectedDetails: &pb.StorageBucketCredentialState{
|
|
||||||
State: &pb.Permissions{
|
|
||||||
Read: &pb.Permission{
|
|
||||||
State: pb.StateType_STATE_TYPE_UNKNOWN,
|
|
||||||
CheckedAt: timestamppb.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fallback-credentials-error",
|
|
||||||
req: &pb.GetObjectRequest{},
|
|
||||||
err: TestAwsS3Error("AccessDenied", "action", "access denied"),
|
|
||||||
expectedStatusCode: codes.PermissionDenied,
|
|
||||||
expectedStatusMsg: "aws service s3: invalid credentials: test:",
|
|
||||||
expectedDetails: &pb.StorageBucketCredentialState{
|
|
||||||
State: &pb.Permissions{
|
|
||||||
Read: &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 := parseS3Error("test", tt.err, tt.req)
|
|
||||||
if tt.expectedStatusCode == codes.OK {
|
|
||||||
require.Nil(actualStatus)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Equal(tt.expectedStatusCode, actualStatus.Code())
|
|
||||||
require.Contains(actualStatus.Message(), tt.expectedStatusMsg)
|
|
||||||
require.Len(actualStatus.Details(), 1)
|
|
||||||
|
|
||||||
foundDetail := false
|
|
||||||
for _, detail := range actualStatus.Details() {
|
|
||||||
switch errDetail := detail.(type) {
|
|
||||||
case *pb.StorageBucketCredentialState:
|
|
||||||
foundDetail = true
|
|
||||||
errors.CheckSimilarPermission(assert, tt.expectedDetails.State.Read, errDetail.State.Read, false)
|
|
||||||
errors.CheckSimilarPermission(assert, tt.expectedDetails.State.Write, errDetail.State.Write, false)
|
|
||||||
errors.CheckSimilarPermission(assert, tt.expectedDetails.State.Delete, errDetail.State.Delete, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
require.True(foundDetail, "did not find error details with status")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+270
-720
File diff suppressed because it is too large.
Load diff
+403
-3008
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
@@ -7,11 +7,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
smithyEndpoints "github.com/aws/smithy-go/endpoints"
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
@@ -22,18 +25,17 @@ type S3API interface {
|
|||||||
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
|
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
|
||||||
HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
|
HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
|
||||||
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type s3Client struct {
|
var customClient = &http.Client{
|
||||||
*s3.Client
|
Transport: &http.Transport{
|
||||||
creds aws.Credentials
|
Proxy: http.ProxyFromEnvironment,
|
||||||
}
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
func (c *s3Client) Credentials() aws.Credentials {
|
KeepAlive: 30 * time.Second,
|
||||||
return c.creds
|
DualStack: true,
|
||||||
|
}).DialContext,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type awsStoragePersistedState struct {
|
type awsStoragePersistedState struct {
|
||||||
@@ -80,6 +82,7 @@ func newAwsStoragePersistedState(opts ...awsStoragePersistedStateOption) (*awsSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *awsStoragePersistedState) toProto() (*storagebuckets.StorageBucketPersisted, error) {
|
func (s *awsStoragePersistedState) toProto() (*storagebuckets.StorageBucketPersisted, error) {
|
||||||
|
|
||||||
data, err := structpb.NewStruct(s.ToMap())
|
data, err := structpb.NewStruct(s.ToMap())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error converting state to structpb.Struct: %w", err)
|
return nil, fmt.Errorf("error converting state to structpb.Struct: %w", err)
|
||||||
@@ -87,40 +90,62 @@ func (s *awsStoragePersistedState) toProto() (*storagebuckets.StorageBucketPersi
|
|||||||
return &storagebuckets.StorageBucketPersisted{Data: data}, nil
|
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.
|
// 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...)
|
opts, err := getOpts(opt...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing options when fetching S3 client: %w", err)
|
return nil, fmt.Errorf("error parsing options when fetching S3 client: %w", err)
|
||||||
}
|
}
|
||||||
awsCfg, err := s.GenerateCredentialChain(ctx)
|
|
||||||
|
awsConfigOpts := []func(*config.LoadOptions) error{}
|
||||||
|
sess, err := s.GetSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting AWS configuration when fetching S3 client: %w", err)
|
return nil, fmt.Errorf("error getting AWS session when fetching S3 client: %w", err)
|
||||||
}
|
}
|
||||||
if awsCfg == nil {
|
creds, err := sess.Config.Credentials.Get()
|
||||||
return nil, fmt.Errorf("nil aws configuration")
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting AWS session when fetching S3 client: %w", err)
|
||||||
}
|
}
|
||||||
var s3Opts []func(*s3.Options)
|
customCredentials := config.WithCredentialsProvider(
|
||||||
if opts.withEndpoint != "" || opts.withDualStack {
|
credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken),
|
||||||
s3Opts = append(s3Opts, s3.WithEndpointResolverV2(&endpointResolver{
|
)
|
||||||
endpoint: opts.withEndpoint,
|
awsConfigOpts = append(awsConfigOpts, customCredentials)
|
||||||
dualStack: opts.withDualStack,
|
if sess.Config.Region != nil {
|
||||||
}))
|
awsConfigOpts = append(awsConfigOpts, config.WithRegion(*sess.Config.Region))
|
||||||
}
|
}
|
||||||
|
if opts.withRegion != "" {
|
||||||
|
awsConfigOpts = append(awsConfigOpts, config.WithRegion(opts.withRegion))
|
||||||
|
}
|
||||||
|
if opts.withEndpoint != "" {
|
||||||
|
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
||||||
|
if service == s3.ServiceID {
|
||||||
|
if opts.withRegion != "" {
|
||||||
|
region = opts.withRegion
|
||||||
|
}
|
||||||
|
return aws.Endpoint{
|
||||||
|
PartitionID: "aws",
|
||||||
|
URL: opts.withEndpoint,
|
||||||
|
SigningRegion: region,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
// returning EndpointNotFoundError will allow the service to fallback to it's default resolution
|
||||||
|
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
|
||||||
|
})
|
||||||
|
awsConfigOpts = append(awsConfigOpts, config.WithEndpointResolverWithOptions(customResolver))
|
||||||
|
}
|
||||||
|
cfg, err := config.LoadDefaultConfig(ctx, awsConfigOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error loading aws configuration when fetching S3 client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if s.testS3APIFunc != nil {
|
if s.testS3APIFunc != nil {
|
||||||
return s.testS3APIFunc(*awsCfg)
|
return s.testS3APIFunc(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the credentials provider from the client
|
cfg.HTTPClient = customClient
|
||||||
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 s3.NewFromConfig(cfg), nil
|
||||||
return &s3Client{Client: c, creds: creds}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getOpts iterates the inbound s3Options and returns a struct
|
// getOpts iterates the inbound s3Options and returns a struct
|
||||||
@@ -142,19 +167,18 @@ type s3Option func(*s3Options) error
|
|||||||
|
|
||||||
// options = how options are represented
|
// options = how options are represented
|
||||||
type s3Options struct {
|
type s3Options struct {
|
||||||
withEndpoint string
|
withRegion string
|
||||||
withDualStack bool
|
withEndpoint string
|
||||||
withCacheRefresh bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultS3Options() s3Options {
|
func getDefaultS3Options() s3Options {
|
||||||
return s3Options{}
|
return s3Options{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithCacheRefresh controls if the cache should be forced to refresh
|
// WithRegion contains the region to use
|
||||||
func WithCacheRefresh(refresh bool) s3Option {
|
func WithRegion(with string) s3Option {
|
||||||
return func(o *s3Options) error {
|
return func(o *s3Options) error {
|
||||||
o.withCacheRefresh = refresh
|
o.withRegion = with
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,39 +190,3 @@ func WithEndpoint(with string) s3Option {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
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 == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
uri, err = url.Parse(e.endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if uri == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resolver = smithyEndpoints.Endpoint{
|
|
||||||
URI: *uri,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
@@ -7,70 +7,37 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/service/s3"
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAwsStoragePersistedStateS3Client(t *testing.T) {
|
func TestAwsStoragePersistedStateS3Client(t *testing.T) {
|
||||||
cases := []struct {
|
require := require.New(t)
|
||||||
name string
|
cred, err := credential.NewAwsCredentialPersistedState(
|
||||||
creds *awsutil.CredentialsConfig
|
credential.WithRegion("us-west-2"),
|
||||||
opts []awsStoragePersistedStateOption
|
credential.WithAccessKeyId("foobar"),
|
||||||
awsOpts []awsutil.Option
|
credential.WithSecretAccessKey("barfoo"),
|
||||||
s3Opts []s3Option
|
)
|
||||||
expectedRegion string
|
require.NoError(err)
|
||||||
expectedEndpoint string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "static credentials",
|
|
||||||
creds: func() *awsutil.CredentialsConfig {
|
|
||||||
creds, err := awsutil.NewCredentialsConfig(
|
|
||||||
awsutil.WithRegion("us-west-2"),
|
|
||||||
awsutil.WithAccessKey("foobar"),
|
|
||||||
awsutil.WithSecretKey("barfoo"),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
return creds
|
|
||||||
}(),
|
|
||||||
awsOpts: []awsutil.Option{
|
|
||||||
awsutil.WithIAMAPIFunc(
|
|
||||||
awsutil.NewMockIAM(),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
opts: []awsStoragePersistedStateOption{
|
|
||||||
withTestS3APIFunc(newTestMockS3(nil)),
|
|
||||||
},
|
|
||||||
s3Opts: []s3Option{
|
|
||||||
WithEndpoint("0.0.0.0"),
|
|
||||||
},
|
|
||||||
expectedRegion: "us-west-2",
|
|
||||||
expectedEndpoint: "0.0.0.0",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range cases {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
cred, err := credential.NewAwsCredentialPersistedState(
|
state, err := newAwsStoragePersistedState(
|
||||||
credential.WithCredentialsConfig(tc.creds),
|
withCredentials(cred),
|
||||||
credential.WithStateTestOpts(tc.awsOpts),
|
withTestS3APIFunc(newTestMockS3(nil)),
|
||||||
)
|
)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
state, err := newAwsStoragePersistedState(
|
clientRaw, err := state.S3Client(context.Background(), WithRegion("us-west-2"), WithEndpoint("0.0.0.0"))
|
||||||
append(tc.opts, withCredentials(cred))...,
|
require.NoError(err)
|
||||||
)
|
require.NotNil(clientRaw)
|
||||||
require.NoError(err)
|
client, ok := clientRaw.(*testMockS3)
|
||||||
|
require.True(ok)
|
||||||
|
require.NotNil(client)
|
||||||
|
require.Equal("us-west-2", client.Region)
|
||||||
|
|
||||||
clientRaw, err := state.s3Client(context.Background(), tc.s3Opts...)
|
endpoint, err := client.Endpoint.ResolveEndpoint(s3.ServiceID, client.Region)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.NotNil(clientRaw)
|
require.Equal("0.0.0.0", endpoint.URL)
|
||||||
client, ok := clientRaw.(*testMockS3)
|
require.Equal("us-west-2", endpoint.SigningRegion)
|
||||||
require.True(ok)
|
require.Equal("aws", endpoint.PartitionID)
|
||||||
require.NotNil(client)
|
|
||||||
require.Equal(tc.expectedRegion, client.Region)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
@@ -13,12 +13,11 @@ import (
|
|||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
"github.com/aws/aws-sdk-go/service/sts"
|
||||||
"github.com/aws/smithy-go"
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
@@ -29,8 +28,22 @@ const (
|
|||||||
testGetUserErr = "test error for GetUser"
|
testGetUserErr = "test error for GetUser"
|
||||||
testGetCallerIdentityErr = "test error for GetCallerIdentity"
|
testGetCallerIdentityErr = "test error for GetCallerIdentity"
|
||||||
testDeleteAccessKeyErr = "test error for DeleteAccessKey"
|
testDeleteAccessKeyErr = "test error for DeleteAccessKey"
|
||||||
|
testGetObjectErr = "test error for GetObject"
|
||||||
|
testPutObjectErr = "test error for PutObject"
|
||||||
|
testHeadObjectErr = "test error for HeadObject"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// throttleErr is a mocked error used for testing the aws s3 client
|
||||||
|
type throttleErr struct{}
|
||||||
|
|
||||||
|
func (e *throttleErr) ErrorCode() string {
|
||||||
|
return "ThrottlingException"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *throttleErr) Error() string {
|
||||||
|
return "ThrottlingException"
|
||||||
|
}
|
||||||
|
|
||||||
type testMockS3State struct {
|
type testMockS3State struct {
|
||||||
GetObjectCalled bool
|
GetObjectCalled bool
|
||||||
GetObjectInputParams *s3.GetObjectInput
|
GetObjectInputParams *s3.GetObjectInput
|
||||||
@@ -41,14 +54,6 @@ type testMockS3State struct {
|
|||||||
|
|
||||||
HeadObjectCalled bool
|
HeadObjectCalled bool
|
||||||
HeadObjectInputParams *s3.HeadObjectInput
|
HeadObjectInputParams *s3.HeadObjectInput
|
||||||
|
|
||||||
ListObjectsV2Called bool
|
|
||||||
ListObjectsV2InputParams *s3.ListObjectsV2Input
|
|
||||||
|
|
||||||
DeleteObjectCalled bool
|
|
||||||
DeleteObjectInputParams *s3.DeleteObjectInput
|
|
||||||
DeleteObjectsCalled bool
|
|
||||||
DeleteObjectsInputParams *s3.DeleteObjectsInput
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *testMockS3State) Reset() {
|
func (s *testMockS3State) Reset() {
|
||||||
@@ -59,19 +64,14 @@ func (s *testMockS3State) Reset() {
|
|||||||
s.PutObjectBody = nil
|
s.PutObjectBody = nil
|
||||||
s.HeadObjectCalled = false
|
s.HeadObjectCalled = false
|
||||||
s.HeadObjectInputParams = nil
|
s.HeadObjectInputParams = nil
|
||||||
s.ListObjectsV2Called = false
|
|
||||||
s.ListObjectsV2InputParams = nil
|
|
||||||
s.DeleteObjectCalled = false
|
|
||||||
s.DeleteObjectInputParams = nil
|
|
||||||
s.DeleteObjectsCalled = false
|
|
||||||
s.DeleteObjectsInputParams = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type testMockS3 struct {
|
type testMockS3 struct {
|
||||||
S3API
|
S3API
|
||||||
|
|
||||||
State *testMockS3State
|
State *testMockS3State
|
||||||
Region string
|
Region string
|
||||||
|
Endpoint aws.EndpointResolverWithOptions
|
||||||
|
|
||||||
// mocked responses for getObject
|
// mocked responses for getObject
|
||||||
GetObjectOutput *s3.GetObjectOutput
|
GetObjectOutput *s3.GetObjectOutput
|
||||||
@@ -84,18 +84,6 @@ type testMockS3 struct {
|
|||||||
// mocked responses for headObject
|
// mocked responses for headObject
|
||||||
HeadObjectOutput *s3.HeadObjectOutput
|
HeadObjectOutput *s3.HeadObjectOutput
|
||||||
HeadObjectErr error
|
HeadObjectErr error
|
||||||
|
|
||||||
// mocked responses for ListObjectsV2 (needed for delete)
|
|
||||||
ListObjectsV2Output *s3.ListObjectsV2Output
|
|
||||||
ListObjectsV2Err error
|
|
||||||
// sometimes we need to create a wildcard output
|
|
||||||
ListObjectsV2OutputFunc func(*s3.ListObjectsV2Input) *s3.ListObjectsV2Output
|
|
||||||
|
|
||||||
// mocked responses for DeleteObject(s)
|
|
||||||
DeleteObjectOutput *s3.DeleteObjectOutput
|
|
||||||
DeleteObjectErr error
|
|
||||||
DeleteObjectsOutput *s3.DeleteObjectsOutput
|
|
||||||
DeleteObjectsErr error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type testMockS3Option func(m *testMockS3) error
|
type testMockS3Option func(m *testMockS3) error
|
||||||
@@ -142,55 +130,6 @@ func testMockS3WithHeadObjectError(e error) testMockS3Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMockS3WithListObjectsV2Output(o *s3.ListObjectsV2Output) testMockS3Option {
|
|
||||||
return func(m *testMockS3) error {
|
|
||||||
m.ListObjectsV2Output = o
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMockS3WithListObjectsV2Error(e error) testMockS3Option {
|
|
||||||
return func(m *testMockS3) error {
|
|
||||||
m.ListObjectsV2Err = e
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMockS3WithListObjectsV2OutputFunc(o func(*s3.ListObjectsV2Input) *s3.ListObjectsV2Output) testMockS3Option {
|
|
||||||
return func(m *testMockS3) error {
|
|
||||||
m.ListObjectsV2OutputFunc = o
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMockS3WithDeleteObjectOutput(o *s3.DeleteObjectOutput) testMockS3Option {
|
|
||||||
return func(m *testMockS3) error {
|
|
||||||
m.DeleteObjectOutput = o
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMockS3WithDeleteObjectError(e error) testMockS3Option {
|
|
||||||
return func(m *testMockS3) error {
|
|
||||||
m.DeleteObjectErr = e
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMockS3WithDeleteObjectsOutput(o *s3.DeleteObjectsOutput) testMockS3Option {
|
|
||||||
return func(m *testMockS3) error {
|
|
||||||
m.DeleteObjectsOutput = o
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMockS3WithDeleteObjectsError(e error) testMockS3Option {
|
|
||||||
return func(m *testMockS3) error {
|
|
||||||
m.DeleteObjectsErr = e
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestMockS3(state *testMockS3State, opts ...testMockS3Option) s3APIFunc {
|
func newTestMockS3(state *testMockS3State, opts ...testMockS3Option) s3APIFunc {
|
||||||
return func(cfgs ...aws.Config) (S3API, error) {
|
return func(cfgs ...aws.Config) (S3API, error) {
|
||||||
m := &testMockS3{
|
m := &testMockS3{
|
||||||
@@ -208,6 +147,9 @@ func newTestMockS3(state *testMockS3State, opts ...testMockS3Option) s3APIFunc {
|
|||||||
if cfg.Region != "" {
|
if cfg.Region != "" {
|
||||||
m.Region = cfg.Region
|
m.Region = cfg.Region
|
||||||
}
|
}
|
||||||
|
if cfg.EndpointResolverWithOptions != nil {
|
||||||
|
m.Endpoint = cfg.EndpointResolverWithOptions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -255,47 +197,8 @@ func (m *testMockS3) PutObject(ctx context.Context, params *s3.PutObjectInput, o
|
|||||||
return m.PutObjectOutput, nil
|
return m.PutObjectOutput, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *testMockS3) ListObjectsV2(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) {
|
|
||||||
if m.State != nil {
|
|
||||||
m.State.ListObjectsV2Called = true
|
|
||||||
m.State.ListObjectsV2InputParams = params
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.ListObjectsV2Err != nil {
|
|
||||||
return nil, m.ListObjectsV2Err
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.ListObjectsV2OutputFunc != nil {
|
|
||||||
return m.ListObjectsV2OutputFunc(params), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.ListObjectsV2Output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *testMockS3) DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
|
func (m *testMockS3) DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
|
||||||
if m.State != nil {
|
return &s3.DeleteObjectOutput{}, nil
|
||||||
m.State.DeleteObjectCalled = true
|
|
||||||
m.State.DeleteObjectInputParams = params
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.DeleteObjectErr != nil {
|
|
||||||
return nil, m.DeleteObjectErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.DeleteObjectOutput, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *testMockS3) DeleteObjects(ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error) {
|
|
||||||
if m.State != nil {
|
|
||||||
m.State.DeleteObjectsCalled = true
|
|
||||||
m.State.DeleteObjectsInputParams = params
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.DeleteObjectsErr != nil {
|
|
||||||
return nil, m.DeleteObjectsErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.DeleteObjectsOutput, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *testMockS3) HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
|
func (m *testMockS3) HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
|
||||||
@@ -398,16 +301,3 @@ func validSTSMock() []credential.AwsCredentialPersistedStateOption {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAwsS3Error returns an s3 api error
|
|
||||||
func TestAwsS3Error(code, op, msg string) error {
|
|
||||||
return &smithy.OperationError{
|
|
||||||
ServiceID: "s3",
|
|
||||||
OperationName: op,
|
|
||||||
Err: &smithy.GenericAPIError{
|
|
||||||
Code: code,
|
|
||||||
Message: msg,
|
|
||||||
Fault: smithy.FaultServer,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+208
-20
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package testing
|
package testing
|
||||||
@@ -11,18 +11,26 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||||
|
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
||||||
"github.com/hashicorp/boundary-plugin-aws/plugin/service/host"
|
"github.com/hashicorp/boundary-plugin-aws/plugin/service/host"
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
|
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const expectedEc2InstanceCount = 5
|
const (
|
||||||
|
expectedIamUserCount = 6
|
||||||
|
expectedEc2InstanceCount = 5
|
||||||
|
)
|
||||||
|
|
||||||
func TestHostPlugin(t *testing.T) {
|
var expectedTags = []string{"foo", "bar", "baz"}
|
||||||
|
|
||||||
|
func TestPlugin(t *testing.T) {
|
||||||
region := os.Getenv("AWS_REGION")
|
region := os.Getenv("AWS_REGION")
|
||||||
if region == "" {
|
if region == "" {
|
||||||
t.Skip("set AWS_REGION to use this test")
|
t.Skip("set AWS_REGION to use this test")
|
||||||
@@ -78,14 +86,6 @@ func TestHostPlugin(t *testing.T) {
|
|||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.Len(ec2InstanceTags, expectedEc2InstanceCount)
|
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
|
// 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
|
// 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
|
// the access key ID/secret access key will no longer be valid. We
|
||||||
@@ -149,13 +149,13 @@ func TestHostPlugin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cases := [][]string{
|
cases := [][]string{
|
||||||
{expectedTags[0]},
|
{"foo"},
|
||||||
{expectedTags[1]},
|
{"bar"},
|
||||||
{expectedTags[2]},
|
{"baz"},
|
||||||
{expectedTags[0], expectedTags[1]},
|
{"foo", "bar"},
|
||||||
{expectedTags[0], expectedTags[2]},
|
{"foo", "baz"},
|
||||||
{expectedTags[1], expectedTags[2]},
|
{"bar", "baz"},
|
||||||
{expectedTags[0], expectedTags[1], expectedTags[2]},
|
{"foo", "bar", "baz"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@@ -195,7 +195,36 @@ func testPluginOnCreateCatalog(ctx context.Context, t *testing.T, p *host.HostPl
|
|||||||
require.NotNil(response)
|
require.NotNil(response)
|
||||||
persisted := response.GetPersisted()
|
persisted := response.GetPersisted()
|
||||||
require.NotNil(persisted)
|
require.NotNil(persisted)
|
||||||
return validatePersistedSecrets(t, persisted.GetSecrets(), accessKeyId, secretAccessKey, rotate)
|
persistedSecrets := persisted.GetSecrets()
|
||||||
|
require.NotNil(persistedSecrets)
|
||||||
|
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotZero(persistedAccessKeyId)
|
||||||
|
if rotate {
|
||||||
|
require.NotEqual(accessKeyId, persistedAccessKeyId)
|
||||||
|
} else {
|
||||||
|
require.Equal(accessKeyId, persistedAccessKeyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotZero(persistedSecretAccessKey)
|
||||||
|
if rotate {
|
||||||
|
require.NotEqual(secretAccessKey, persistedSecretAccessKey)
|
||||||
|
} else {
|
||||||
|
require.Equal(secretAccessKey, persistedSecretAccessKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||||
|
require.NoError(err)
|
||||||
|
if rotate {
|
||||||
|
require.NotZero(persistedCredsLastRotatedTime)
|
||||||
|
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
||||||
|
} else {
|
||||||
|
require.Zero(persistedCredsLastRotatedTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedAccessKeyId, persistedSecretAccessKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPluginOnUpdateCatalog(
|
func testPluginOnUpdateCatalog(
|
||||||
@@ -264,7 +293,116 @@ func testPluginOnUpdateCatalog(
|
|||||||
require.NotNil(response)
|
require.NotNil(response)
|
||||||
persisted := response.GetPersisted()
|
persisted := response.GetPersisted()
|
||||||
require.NotNil(persisted)
|
require.NotNil(persisted)
|
||||||
return validateUpdateSecrets(t, persisted.GetSecrets(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
|
persistedSecrets := persisted.GetSecrets()
|
||||||
|
require.NotNil(persistedSecrets)
|
||||||
|
|
||||||
|
// Complex checks based on the scenarios.
|
||||||
|
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotZero(persistedAccessKeyId)
|
||||||
|
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotZero(persistedSecretAccessKey)
|
||||||
|
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
// Our test scenarios are complex due the multi-dimensional nature
|
||||||
|
// of criteria, so we lay them out in a switch below.
|
||||||
|
switch {
|
||||||
|
case newAccessKeyId != "" && rotated && rotate:
|
||||||
|
// The new access key ID was provided, we had previously rotated
|
||||||
|
// the credentials before, and the new credential set is to be
|
||||||
|
// rotated as well. In this case, the old credentials should have
|
||||||
|
// been deleted, and the new credentials should have been rotated,
|
||||||
|
// hence, should not match the new credentials initially
|
||||||
|
// provided. Rotation time should be non-zero and updated.
|
||||||
|
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||||
|
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||||
|
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||||
|
require.NotZero(persistedCredsLastRotatedTime)
|
||||||
|
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||||
|
|
||||||
|
case newAccessKeyId != "" && rotated && !rotate:
|
||||||
|
// The new access key ID was provided, we had previously rotated
|
||||||
|
// the credentials before, and the new credential is *not*
|
||||||
|
// rotated. In this case, the old credentials should have
|
||||||
|
// been deleted, But the new credentials should have not been
|
||||||
|
// rotated, and hence should be the same. Rotation time should be
|
||||||
|
// zero.
|
||||||
|
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||||
|
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||||
|
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||||
|
require.Zero(persistedCredsLastRotatedTime)
|
||||||
|
|
||||||
|
case newAccessKeyId != "" && !rotated && rotate:
|
||||||
|
// The new access key ID was provided, we *have not* previously
|
||||||
|
// rotated the credentials, and the new credential set is to be
|
||||||
|
// rotated. In this case, the old credentials should have been
|
||||||
|
// left alone, and the new credentials should have been rotated,
|
||||||
|
// hence, should not match the new credentials initially
|
||||||
|
// provided. Rotation time should be non-zero, but updated.
|
||||||
|
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||||
|
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
||||||
|
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
||||||
|
require.NotZero(persistedCredsLastRotatedTime)
|
||||||
|
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
||||||
|
|
||||||
|
case newAccessKeyId != "" && !rotated && !rotate:
|
||||||
|
// The new access key ID was provided, but we have not rotated
|
||||||
|
// the credentials previously and we still don't plan on rotating
|
||||||
|
// them. In this case, the old credentials should still be valid,
|
||||||
|
// and the persisted ones should match the new ones provided.
|
||||||
|
// Rotation time should be zero.
|
||||||
|
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||||
|
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
||||||
|
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
||||||
|
require.Zero(persistedCredsLastRotatedTime)
|
||||||
|
|
||||||
|
case newAccessKeyId == "" && rotated && rotate:
|
||||||
|
// No new credentials have been provided, but we have previously
|
||||||
|
// rotated and are still rotating credentials. This is a no-op.
|
||||||
|
// Existing credentials should still be valid and match the ones
|
||||||
|
// persisted to state. Rotation time should be identical since
|
||||||
|
// no new rotation occurred.
|
||||||
|
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||||
|
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||||
|
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||||
|
require.NotZero(persistedCredsLastRotatedTime)
|
||||||
|
require.True(currentCredsLastRotatedTime.Equal(persistedCredsLastRotatedTime))
|
||||||
|
|
||||||
|
case newAccessKeyId == "" && rotated && !rotate:
|
||||||
|
// No new credentials have been provided, and we have previously
|
||||||
|
// rotated the credentials. This is actually an error, but we
|
||||||
|
// don't test it here; it's covered in unit testing (see
|
||||||
|
// plugin_test.go).
|
||||||
|
require.FailNow("testing rotated-to-not-rotated scenario not implemented by this helper")
|
||||||
|
|
||||||
|
case newAccessKeyId == "" && !rotated && rotate:
|
||||||
|
// No new credentials have been provided, and while we did not
|
||||||
|
// rotate before, we want to switch to rotation. In this case,
|
||||||
|
// the existing persisted credentials should have been rotated,
|
||||||
|
// with a new non-zero timestamp.
|
||||||
|
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||||
|
require.NotEqual(persistedAccessKeyId, currentAccessKeyId)
|
||||||
|
require.NotEqual(persistedSecretAccessKey, currentSecretAccessKey)
|
||||||
|
require.NotZero(persistedCredsLastRotatedTime)
|
||||||
|
|
||||||
|
case newAccessKeyId == "" && !rotated && !rotate:
|
||||||
|
// No new credentials have been provided and we have not, nor do
|
||||||
|
// not, plan on rotating the credentials. This is a no-op.
|
||||||
|
// Existing credentials should still be valid and match the ones
|
||||||
|
// persisted to state. Rotation time should remain at zero.
|
||||||
|
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
||||||
|
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
||||||
|
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
||||||
|
require.Zero(persistedCredsLastRotatedTime)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Scenario was reached that was not covered by this function.
|
||||||
|
require.FailNow("unknown test scenario")
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedAccessKeyId, persistedSecretAccessKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPluginOnDeleteCatalog(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, rotated bool) {
|
func testPluginOnDeleteCatalog(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, rotated bool) {
|
||||||
@@ -450,3 +588,53 @@ func testPluginListHosts(ctx context.Context, t *testing.T, p *host.HostPlugin,
|
|||||||
// Success
|
// Success
|
||||||
t.Logf("testing ListHosts: success (region=%s, tags=%v, expected/actual=(len=%d, ids=%s))", region, tags, len(actualInstances), actualInstances)
|
t.Logf("testing ListHosts: success (region=%s, tags=%v, expected/actual=(len=%d, ids=%s))", region, tags, len(actualInstances), actualInstances)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||||
|
t.Helper()
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
c, err := awsutil.NewCredentialsConfig(
|
||||||
|
awsutil.WithAccessKey(accessKeyId),
|
||||||
|
awsutil.WithSecretKey(secretAccessKey),
|
||||||
|
)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
// We need to wait for invalidation as while awsutil waits for
|
||||||
|
// credential creation, deletion of the old credentials returns
|
||||||
|
// immediately.
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
waitErr:
|
||||||
|
for {
|
||||||
|
_, err = c.GetCallerIdentity()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
// pass
|
||||||
|
|
||||||
|
case <-timeoutCtx.Done():
|
||||||
|
break waitErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotNil(err)
|
||||||
|
awsErr, ok := err.(awserr.Error)
|
||||||
|
require.True(ok)
|
||||||
|
require.Equal("InvalidClientTokenId", awsErr.Code())
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) {
|
||||||
|
t.Helper()
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
c, err := awsutil.NewCredentialsConfig(
|
||||||
|
awsutil.WithAccessKey(accessKeyId),
|
||||||
|
awsutil.WithSecretKey(secretAccessKey),
|
||||||
|
)
|
||||||
|
require.NoError(err)
|
||||||
|
_, err = c.GetCallerIdentity()
|
||||||
|
require.NoError(err)
|
||||||
|
}
|
||||||
@@ -1,723 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package testing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
|
||||||
"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 == "" {
|
|
||||||
t.Skip("set AWS_REGION to use this test")
|
|
||||||
}
|
|
||||||
if envAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID"); envAccessKeyId == "" {
|
|
||||||
t.Skip("set AWS_ACCESS_KEY_ID to use this test")
|
|
||||||
}
|
|
||||||
if envSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY"); envSecretAccessKey == "" {
|
|
||||||
t.Skip("set AWS_SECRET_ACCESS_KEY to use this test")
|
|
||||||
}
|
|
||||||
|
|
||||||
require := require.New(t)
|
|
||||||
tf, err := NewTestTerraformer("testdata/storage")
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(tf)
|
|
||||||
|
|
||||||
t.Log("===== deploying test Terraform workspace =====")
|
|
||||||
err = tf.Deploy()
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
t.Log("===== destroying test Terraform workspace =====")
|
|
||||||
if err := tf.Destroy(); err != nil {
|
|
||||||
t.Logf("WARNING: could not run Terraform destroy: %s", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
bucketName, err := tf.GetOutputString("bucket_name")
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotEmpty(bucketName)
|
|
||||||
|
|
||||||
iamAccessKeyIds, err := tf.GetOutputSlice("iam_access_key_ids")
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(iamAccessKeyIds, expectedIamUserCount)
|
|
||||||
|
|
||||||
iamSecretAccessKeys, err := tf.GetOutputSlice("iam_secret_access_keys")
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(iamSecretAccessKeys, expectedIamUserCount)
|
|
||||||
|
|
||||||
iamAccessKeyMissingGetObject, err := tf.GetOutputString("iam_access_key_missing_get_obj")
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
iamSecretAccessKeyMissingGetObject, err := tf.GetOutputString("iam_secret_access_key_missing_get_obj")
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
iamAccessKeyMissingPutObject, err := tf.GetOutputString("iam_access_key_missing_put_obj")
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
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
|
|
||||||
// will assert this through the returned state.
|
|
||||||
p := new(storage.StoragePlugin)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// ********************
|
|
||||||
// * OnCreateStorageBucket
|
|
||||||
// ********************
|
|
||||||
//
|
|
||||||
// Test non-rotation (using primary user).
|
|
||||||
keyid, secret := testPluginOnCreateStorageBucket(ctx, t, p, bucketName, region, iamAccessKeyIds[0].(string), iamSecretAccessKeys[0].(string), false)
|
|
||||||
// Test rotation next.
|
|
||||||
keyid, secret = testPluginOnCreateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
|
|
||||||
|
|
||||||
// ********************
|
|
||||||
// * OnUpdateStorageBucket
|
|
||||||
// ********************
|
|
||||||
//
|
|
||||||
// Test no-op non-rotation.
|
|
||||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", false, false)
|
|
||||||
// Switch to rotation.
|
|
||||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", false, true)
|
|
||||||
// Test no-op with rotation.
|
|
||||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, "", "", true, true)
|
|
||||||
// Switch credentials to next user. Don't rotate.
|
|
||||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[1].(string), iamSecretAccessKeys[1].(string), true, false)
|
|
||||||
// Switch credentials to next user. Add rotation.
|
|
||||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[2].(string), iamSecretAccessKeys[2].(string), false, true)
|
|
||||||
// Switch to next user, with rotation disabled.
|
|
||||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[3].(string), iamSecretAccessKeys[3].(string), true, false)
|
|
||||||
// Last case - switch to another user and keep rotation off.
|
|
||||||
keyid, secret = testPluginOnUpdateStorageBucket(ctx, t, p, bucketName, region, keyid, secret, iamAccessKeyIds[4].(string), iamSecretAccessKeys[4].(string), false, false)
|
|
||||||
|
|
||||||
// ********************
|
|
||||||
// * OnDeleteStorageBucket
|
|
||||||
// ********************
|
|
||||||
//
|
|
||||||
// Test non-rotated.
|
|
||||||
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, false)
|
|
||||||
// Test as if we had rotated these credentials (note that this
|
|
||||||
// makes this test set unusable).
|
|
||||||
testPluginOnDeleteStorageBucket(ctx, t, p, bucketName, region, keyid, secret, true)
|
|
||||||
|
|
||||||
// ********************
|
|
||||||
// * 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
|
|
||||||
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
|
|
||||||
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) {
|
|
||||||
t.Helper()
|
|
||||||
t.Logf("testing OnCreateStorageBucket (region=%s, rotate=%t)", region, rotate)
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: !rotate,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: accessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: secretAccessKey,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
request := &pb.OnCreateStorageBucketRequest{
|
|
||||||
Bucket: &storagebuckets.StorageBucket{
|
|
||||||
BucketName: bucketName,
|
|
||||||
Secrets: reqSecrets,
|
|
||||||
Attributes: reqAttrs,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
response, err := p.OnCreateStorageBucket(ctx, request)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(response)
|
|
||||||
persisted := response.GetPersisted()
|
|
||||||
require.NotNil(persisted)
|
|
||||||
return validatePersistedSecrets(t, persisted.GetData(), accessKeyId, secretAccessKey, rotate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPluginOnUpdateStorageBucket(
|
|
||||||
ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName,
|
|
||||||
region, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
|
|
||||||
rotated, rotate bool,
|
|
||||||
) (string, string) {
|
|
||||||
t.Helper()
|
|
||||||
t.Logf("testing OnUpdateStorageBucket (region=%s, newcreds=%t, rotated=%t, rotate=%t)", region, newAccessKeyId != "" && newSecretAccessKey != "", rotated, rotate)
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
// Take a timestamp of the current time to get a point in time to
|
|
||||||
// reference, ensuring that we are updating credential rotation
|
|
||||||
// timestamps.
|
|
||||||
currentCredsLastRotatedTime := time.Now()
|
|
||||||
|
|
||||||
reqCurrentAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: !rotated,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
reqNewAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: !rotate,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
var reqSecrets *structpb.Struct
|
|
||||||
if newAccessKeyId != "" && newSecretAccessKey != "" {
|
|
||||||
reqSecrets, err = structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: newAccessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: newSecretAccessKey,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
}
|
|
||||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: currentAccessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: currentSecretAccessKey,
|
|
||||||
credential.ConstCredsLastRotatedTime: func() string {
|
|
||||||
if rotated {
|
|
||||||
return currentCredsLastRotatedTime.Format(time.RFC3339Nano)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (time.Time{}).Format(time.RFC3339Nano)
|
|
||||||
}(),
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(reqPersistedSecrets)
|
|
||||||
request := &pb.OnUpdateStorageBucketRequest{
|
|
||||||
CurrentBucket: &storagebuckets.StorageBucket{
|
|
||||||
BucketName: bucketName,
|
|
||||||
Attributes: reqCurrentAttrs,
|
|
||||||
},
|
|
||||||
NewBucket: &storagebuckets.StorageBucket{
|
|
||||||
BucketName: bucketName,
|
|
||||||
Attributes: reqNewAttrs,
|
|
||||||
Secrets: reqSecrets,
|
|
||||||
},
|
|
||||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
|
||||||
Data: reqPersistedSecrets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
response, err := p.OnUpdateStorageBucket(ctx, request)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(response)
|
|
||||||
persisted := response.GetPersisted()
|
|
||||||
require.NotNil(persisted)
|
|
||||||
return validateUpdateSecrets(t, persisted.GetData(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPluginOnDeleteStorageBucket(ctx context.Context, t *testing.T, p *storage.StoragePlugin, bucketName, region, accessKeyId, secretAccessKey string, rotated bool) {
|
|
||||||
t.Helper()
|
|
||||||
t.Logf("testing OnDeleteStorageBucket (region=%s, rotated=%t)", region, rotated)
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: !rotated,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: accessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: secretAccessKey,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: accessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: secretAccessKey,
|
|
||||||
credential.ConstCredsLastRotatedTime: func() string {
|
|
||||||
if rotated {
|
|
||||||
return time.Now().Format(time.RFC3339Nano)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (time.Time{}).Format(time.RFC3339Nano)
|
|
||||||
}(),
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
request := &pb.OnDeleteStorageBucketRequest{
|
|
||||||
Bucket: &storagebuckets.StorageBucket{
|
|
||||||
BucketName: bucketName,
|
|
||||||
Attributes: reqAttrs,
|
|
||||||
Secrets: reqSecrets,
|
|
||||||
},
|
|
||||||
Persisted: &storagebuckets.StorageBucketPersisted{
|
|
||||||
Data: reqPersistedSecrets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
response, err := p.OnDeleteStorageBucket(ctx, request)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(response)
|
|
||||||
|
|
||||||
// We want to test the validity of the credentials post-deletion.
|
|
||||||
if rotated {
|
|
||||||
// The credentials should no longer be valid.
|
|
||||||
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
|
||||||
} else {
|
|
||||||
// The credentials should still be valid. Sleep 10s first just to
|
|
||||||
// be sure, since we're not rotating.
|
|
||||||
time.Sleep(time.Second * 10)
|
|
||||||
requireCredentialsValid(t, accessKeyId, secretAccessKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, assert := require.New(t), assert.New(t)
|
|
||||||
|
|
||||||
require.NotNil(expectedState)
|
|
||||||
|
|
||||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: true,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: accessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: secretAccessKey,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
req := &pb.ValidatePermissionsRequest{
|
|
||||||
Bucket: &storagebuckets.StorageBucket{
|
|
||||||
BucketName: bucketName,
|
|
||||||
Attributes: reqAttrs,
|
|
||||||
Secrets: reqSecrets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, 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) {
|
|
||||||
t.Helper()
|
|
||||||
t.Logf("testing PutObject")
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
testDataPath, err := filepath.Abs("./testdata/storage/test_object_data")
|
|
||||||
require.NoError(err)
|
|
||||||
require.FileExists(testDataPath)
|
|
||||||
|
|
||||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: true,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: accessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: secretAccessKey,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
objectKey := uuid.New().String()
|
|
||||||
putObjReq := &pb.PutObjectRequest{
|
|
||||||
Bucket: &storagebuckets.StorageBucket{
|
|
||||||
BucketName: bucketName,
|
|
||||||
Attributes: reqAttrs,
|
|
||||||
Secrets: reqSecrets,
|
|
||||||
},
|
|
||||||
Key: objectKey,
|
|
||||||
Path: testDataPath,
|
|
||||||
}
|
|
||||||
putObjResp, err := p.PutObject(ctx, putObjReq)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(putObjResp)
|
|
||||||
require.NotEmpty(putObjResp.GetChecksumSha_256())
|
|
||||||
|
|
||||||
t.Logf("testing HeadObject")
|
|
||||||
headObjReq := &pb.HeadObjectRequest{
|
|
||||||
Bucket: &storagebuckets.StorageBucket{
|
|
||||||
BucketName: bucketName,
|
|
||||||
Attributes: reqAttrs,
|
|
||||||
Secrets: reqSecrets,
|
|
||||||
},
|
|
||||||
Key: objectKey,
|
|
||||||
}
|
|
||||||
headObjResp, err := p.HeadObject(ctx, headObjReq)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(headObjResp)
|
|
||||||
require.NotEmpty(headObjResp.GetContentLength())
|
|
||||||
require.NotEmpty(headObjResp.GetLastModified())
|
|
||||||
|
|
||||||
// Need to create a GRPC server to test GetObject
|
|
||||||
t.Logf("testing GetObject")
|
|
||||||
lis, err := net.Listen("tcp", "[::1]:2030")
|
|
||||||
require.NoError(err)
|
|
||||||
grpcServer := grpc.NewServer()
|
|
||||||
pb.RegisterStoragePluginServiceServer(grpcServer, &storage.StoragePlugin{})
|
|
||||||
go func() {
|
|
||||||
grpcServer.Serve(lis)
|
|
||||||
}()
|
|
||||||
|
|
||||||
conn, err := grpc.Dial("[::1]:2030", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
||||||
require.NoError(err)
|
|
||||||
defer conn.Close()
|
|
||||||
client := pb.NewStoragePluginServiceClient(conn)
|
|
||||||
|
|
||||||
stream, err := client.GetObject(ctx, &pb.GetObjectRequest{
|
|
||||||
Bucket: &storagebuckets.StorageBucket{
|
|
||||||
BucketName: bucketName,
|
|
||||||
Attributes: reqAttrs,
|
|
||||||
Secrets: reqSecrets,
|
|
||||||
},
|
|
||||||
Key: objectKey,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
var actualData []byte
|
|
||||||
expectedData, err := os.ReadFile(testDataPath)
|
|
||||||
require.NoError(err)
|
|
||||||
for {
|
|
||||||
resp, err := stream.Recv()
|
|
||||||
if err == io.EOF {
|
|
||||||
require.NoError(stream.CloseSend())
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
require.NoError(err)
|
|
||||||
}
|
|
||||||
actualData = append(actualData, resp.GetFileChunk()...)
|
|
||||||
}
|
|
||||||
require.Equal(expectedData, actualData)
|
|
||||||
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
+15
-22
@@ -1,20 +1,24 @@
|
|||||||
# Copyright IBM Corp. 2021, 2026
|
# Copyright (c) HashiCorp, Inc.
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
# 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" {
|
resource "random_id" "aws_iam_user_name" {
|
||||||
count = var.iam_user_count
|
count = var.iam_user_count
|
||||||
prefix = "demo-${local.hashicorp_email}-boundary-iam-user"
|
prefix = "${var.iam_user_prefix}-${count.index}"
|
||||||
byte_length = 4
|
byte_length = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_iam_user" "user" {
|
resource "aws_iam_user" "user" {
|
||||||
count = var.iam_user_count
|
count = var.iam_user_count
|
||||||
name = random_id.aws_iam_user_name[count.index].dec
|
name = random_id.aws_iam_user_name[count.index].dec
|
||||||
force_destroy = true
|
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" {
|
resource "aws_iam_access_key" "user_initial_key" {
|
||||||
@@ -22,13 +26,8 @@ resource "aws_iam_access_key" "user_initial_key" {
|
|||||||
user = aws_iam_user.user[count.index].name
|
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" {
|
resource "aws_iam_policy" "ec2_describeinstances" {
|
||||||
name = random_id.aws_ec2_policy_name.dec
|
name = "BoundaryPluginHostAwsTestDescribeInstancesPolicy"
|
||||||
|
|
||||||
policy = <<EOF
|
policy = <<EOF
|
||||||
{
|
{
|
||||||
@@ -52,15 +51,9 @@ resource "aws_iam_user_policy_attachment" "user_ec2_describeinstances_attachment
|
|||||||
policy_arn = aws_iam_policy.ec2_describeinstances.arn
|
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" {
|
resource "aws_iam_policy" "user_self_manage_policy" {
|
||||||
count = var.iam_user_count
|
count = var.iam_user_count
|
||||||
name = random_id.aws_iam_policy_name[count.index].dec
|
name = "BoundaryPluginHostAwsTestUserSelfManagePolicy-${count.index}"
|
||||||
|
|
||||||
policy = <<EOF
|
policy = <<EOF
|
||||||
{
|
{
|
||||||
|
|||||||
Vendored
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
# Copyright IBM Corp. 2021, 2026
|
# Copyright (c) HashiCorp, Inc.
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
aws = {
|
aws = {
|
||||||
source = "hashicorp/aws"
|
source = "hashicorp/aws"
|
||||||
version = "~> 6.0"
|
version = "~> 3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
-49
@@ -1,49 +0,0 @@
|
|||||||
# 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
+26
-12
@@ -1,6 +1,28 @@
|
|||||||
# Copyright IBM Corp. 2021, 2026
|
# Copyright (c) HashiCorp, Inc.
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
# 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_availability_zones" "azs" {}
|
||||||
|
|
||||||
data "aws_ami" "ubuntu" {
|
data "aws_ami" "ubuntu" {
|
||||||
@@ -30,12 +52,12 @@ resource "aws_subnet" "subnet" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_instance" "instances" {
|
resource "aws_instance" "instances" {
|
||||||
count = length(local.instance_tags)
|
count = length(var.instance_tags)
|
||||||
ami = data.aws_ami.ubuntu.id
|
ami = data.aws_ami.ubuntu.id
|
||||||
instance_type = "t3.nano"
|
instance_type = "t3.nano"
|
||||||
subnet_id = aws_subnet.subnet.id
|
subnet_id = aws_subnet.subnet.id
|
||||||
|
|
||||||
tags = local.instance_tags[count.index]
|
tags = var.instance_tags[count.index]
|
||||||
}
|
}
|
||||||
|
|
||||||
output "instance_ids" {
|
output "instance_ids" {
|
||||||
@@ -52,12 +74,4 @@ output "instance_tags" {
|
|||||||
value = {
|
value = {
|
||||||
for _, r in aws_instance.instances : r.id => r.tags
|
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 IBM Corp. 2021, 2026
|
# Copyright (c) HashiCorp, Inc.
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
resource "random_id" "foo" {
|
resource "random_id" "foo" {
|
||||||
|
|||||||
Vendored
-327
@@ -1,327 +0,0 @@
|
|||||||
# Copyright IBM Corp. 2021, 2026
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
resource "aws_iam_role" "valid" {
|
|
||||||
name = "${random_id.prefix.dec}-valid"
|
|
||||||
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" "valid" {
|
|
||||||
name = "${random_id.prefix.dec}-valid"
|
|
||||||
tags = local.tags
|
|
||||||
|
|
||||||
policy = <<EOT
|
|
||||||
{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Action": [
|
|
||||||
"s3:PutObject",
|
|
||||||
"s3:GetObject",
|
|
||||||
"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}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
}
|
|
||||||
EOT
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role_policy_attachment" "valid" {
|
|
||||||
role = aws_iam_role.valid.name
|
|
||||||
policy_arn = aws_iam_policy.valid.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role" "missing_put_obj" {
|
|
||||||
name = "${random_id.prefix.dec}-missing-put-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_put_obj" {
|
|
||||||
name = "${random_id.prefix.dec}-missing-put-obj"
|
|
||||||
tags = local.tags
|
|
||||||
|
|
||||||
policy = <<EOT
|
|
||||||
{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Action": [
|
|
||||||
"s3:GetObject",
|
|
||||||
"s3:GetObjectAttributes",
|
|
||||||
"s3:ListBucket",
|
|
||||||
"s3:DeleteObject"
|
|
||||||
],
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
}
|
|
||||||
EOT
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role_policy_attachment" "missing_put_obj" {
|
|
||||||
role = aws_iam_role.missing_put_obj.name
|
|
||||||
policy_arn = aws_iam_policy.missing_put_obj.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role" "missing_get_obj" {
|
|
||||||
name = "${random_id.prefix.dec}-missing-get-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_get_obj" {
|
|
||||||
name = "${random_id.prefix.dec}-missing-get-obj"
|
|
||||||
tags = local.tags
|
|
||||||
|
|
||||||
policy = <<EOT
|
|
||||||
{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Action": [
|
|
||||||
"s3:PutObject",
|
|
||||||
"s3:DeleteObject"
|
|
||||||
],
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Resource": "${aws_s3_bucket.test.arn}*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
}
|
|
||||||
EOT
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role_policy_attachment" "missing_get_obj" {
|
|
||||||
role = aws_iam_role.missing_get_obj.name
|
|
||||||
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 = "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
|
|
||||||
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" {
|
|
||||||
count = var.iam_user_count
|
|
||||||
user = aws_iam_user.test[count.index].name
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_user_policy_attachment" "test_s3" {
|
|
||||||
count = var.iam_user_count
|
|
||||||
user = aws_iam_user.test[count.index].name
|
|
||||||
policy_arn = aws_iam_policy.valid.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_policy" "credentials" {
|
|
||||||
count = var.iam_user_count
|
|
||||||
name = aws_iam_user.test[count.index].name
|
|
||||||
tags = local.tags
|
|
||||||
|
|
||||||
policy = <<EOF
|
|
||||||
{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Action": [
|
|
||||||
"iam:DeleteAccessKey",
|
|
||||||
"iam:GetUser",
|
|
||||||
"iam:CreateAccessKey"
|
|
||||||
],
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Resource": "${aws_iam_user.test[count.index].arn}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_user_policy_attachment" "test_credentials" {
|
|
||||||
count = var.iam_user_count
|
|
||||||
user = aws_iam_user.test[count.index].name
|
|
||||||
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 = "demo-${local.hashicorp_email}" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
|
|
||||||
byte_length = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_user" "missing_get_obj" {
|
|
||||||
name = random_id.missing_get_obj.dec
|
|
||||||
force_destroy = true
|
|
||||||
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser" # 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" {
|
|
||||||
user = aws_iam_user.missing_get_obj.name
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_user_policy_attachment" "missing_get_obj" {
|
|
||||||
user = aws_iam_user.missing_get_obj.name
|
|
||||||
policy_arn = aws_iam_policy.missing_get_obj.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# Edge case for missing PutObject permission
|
|
||||||
|
|
||||||
resource "random_id" "missing_put_obj" {
|
|
||||||
prefix = "demo-${local.hashicorp_email}" # Do Not Remove/Edit. This is a requirement to obtain access in creating a iam user in the dev account.
|
|
||||||
byte_length = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_user" "missing_put_obj" {
|
|
||||||
name = random_id.missing_put_obj.dec
|
|
||||||
force_destroy = true
|
|
||||||
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/DemoUser" # 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" {
|
|
||||||
user = aws_iam_user.missing_put_obj.name
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_user_policy_attachment" "missing_put_obj" {
|
|
||||||
user = aws_iam_user.missing_put_obj.name
|
|
||||||
policy_arn = aws_iam_policy.missing_put_obj.arn
|
|
||||||
}
|
|
||||||
Vendored
-11
@@ -1,11 +0,0 @@
|
|||||||
# Copyright IBM Corp. 2021, 2026
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
aws = {
|
|
||||||
source = "hashicorp/aws"
|
|
||||||
version = "~> 6.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
-75
@@ -1,75 +0,0 @@
|
|||||||
# Copyright IBM Corp. 2021, 2026
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
output "bucket_name" {
|
|
||||||
value = aws_s3_bucket.test.id
|
|
||||||
}
|
|
||||||
|
|
||||||
output "valid_policy_arn" {
|
|
||||||
value = aws_iam_role.valid.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
output "missing_put_obj_policy_arn" {
|
|
||||||
value = aws_iam_role.missing_put_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" {
|
|
||||||
value = aws_iam_user.test.*.name
|
|
||||||
}
|
|
||||||
|
|
||||||
output "iam_user_arns" {
|
|
||||||
value = aws_iam_user.test.*.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
output "iam_access_key_ids" {
|
|
||||||
value = aws_iam_access_key.test.*.id
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
output "iam_secret_access_keys" {
|
|
||||||
value = aws_iam_access_key.test.*.secret
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
output "iam_access_key_missing_put_obj" {
|
|
||||||
value = aws_iam_access_key.missing_put_obj.id
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
output "iam_secret_access_key_missing_put_obj" {
|
|
||||||
value = aws_iam_access_key.missing_put_obj.secret
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
output "iam_access_key_missing_get_obj" {
|
|
||||||
value = aws_iam_access_key.missing_get_obj.id
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
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
-25
@@ -1,25 +0,0 @@
|
|||||||
# Copyright IBM Corp. 2021, 2026
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
resource "aws_s3_bucket" "test" {
|
|
||||||
bucket = random_id.prefix.dec
|
|
||||||
force_destroy = true
|
|
||||||
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
|
|
||||||
}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
HELLO_WORLD!
|
|
||||||
Vendored
-62
@@ -1,62 +0,0 @@
|
|||||||
# Copyright IBM Corp. 2021, 2026
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
data "aws_caller_identity" "current" {}
|
|
||||||
|
|
||||||
resource "random_id" "prefix" {
|
|
||||||
prefix = "aws-plugin-e2e-test"
|
|
||||||
byte_length = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "iam_user_count" {
|
|
||||||
default = 6
|
|
||||||
}
|
|
||||||
|
|
||||||
locals {
|
|
||||||
hashicorp_email = split(":", data.aws_caller_identity.current.user_id)[1]
|
|
||||||
tags = {
|
|
||||||
User = split(":", data.aws_caller_identity.current.user_id)[1]
|
|
||||||
Repo = "boundary-plugin-aws"
|
|
||||||
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
-213
@@ -1,29 +1,18 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package testing
|
package testing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/aws/smithy-go"
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
|
||||||
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
|
||||||
tfjson "github.com/hashicorp/terraform-json"
|
tfjson "github.com/hashicorp/terraform-json"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const expectedIamUserCount = 6
|
|
||||||
|
|
||||||
// TestTerraformer is an object representing the lifecycle of a
|
// TestTerraformer is an object representing the lifecycle of a
|
||||||
// particular Terraform project.
|
// particular Terraform project.
|
||||||
//
|
//
|
||||||
@@ -275,204 +264,3 @@ func (t *TestTerraformer) showJson() ([]byte, error) {
|
|||||||
|
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
|
|
||||||
t.Helper()
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
c, err := awsutil.NewCredentialsConfig(
|
|
||||||
awsutil.WithAccessKey(accessKeyId),
|
|
||||||
awsutil.WithSecretKey(secretAccessKey),
|
|
||||||
)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
// We need to wait for invalidation as while awsutil waits for
|
|
||||||
// credential creation, deletion of the old credentials returns
|
|
||||||
// immediately.
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
|
||||||
defer cancel()
|
|
||||||
waitErr:
|
|
||||||
for {
|
|
||||||
_, err = c.GetCallerIdentity(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
// pass
|
|
||||||
|
|
||||||
case <-timeoutCtx.Done():
|
|
||||||
break waitErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NotNil(err)
|
|
||||||
var ae smithy.APIError
|
|
||||||
require.True(errors.As(err, &ae))
|
|
||||||
require.Equal("InvalidClientTokenId", ae.ErrorCode())
|
|
||||||
}
|
|
||||||
|
|
||||||
func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) {
|
|
||||||
t.Helper()
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
c, err := awsutil.NewCredentialsConfig(
|
|
||||||
awsutil.WithAccessKey(accessKeyId),
|
|
||||||
awsutil.WithSecretKey(secretAccessKey),
|
|
||||||
)
|
|
||||||
require.NoError(err)
|
|
||||||
_, err = c.GetCallerIdentity(context.Background())
|
|
||||||
require.NoError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validatePersistedSecrets(t *testing.T, persistedSecrets *structpb.Struct, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
|
||||||
t.Helper()
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
require.NotNil(persistedSecrets)
|
|
||||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotZero(persistedAccessKeyId)
|
|
||||||
if rotate {
|
|
||||||
require.NotEqual(accessKeyId, persistedAccessKeyId)
|
|
||||||
} else {
|
|
||||||
require.Equal(accessKeyId, persistedAccessKeyId)
|
|
||||||
}
|
|
||||||
|
|
||||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotZero(persistedSecretAccessKey)
|
|
||||||
if rotate {
|
|
||||||
require.NotEqual(secretAccessKey, persistedSecretAccessKey)
|
|
||||||
} else {
|
|
||||||
require.Equal(secretAccessKey, persistedSecretAccessKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
|
||||||
require.NoError(err)
|
|
||||||
if rotate {
|
|
||||||
require.NotZero(persistedCredsLastRotatedTime)
|
|
||||||
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
|
||||||
} else {
|
|
||||||
require.Zero(persistedCredsLastRotatedTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
return persistedAccessKeyId, persistedSecretAccessKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateUpdateSecrets(
|
|
||||||
t *testing.T, persistedSecrets *structpb.Struct, currentCredsLastRotatedTime time.Time,
|
|
||||||
currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
|
|
||||||
rotated, rotate bool) (string, string) {
|
|
||||||
t.Helper()
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
// Complex checks based on the scenarios.
|
|
||||||
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotZero(persistedAccessKeyId)
|
|
||||||
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotZero(persistedSecretAccessKey)
|
|
||||||
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
// Our test scenarios are complex due the multi-dimensional nature
|
|
||||||
// of criteria, so we lay them out in a switch below.
|
|
||||||
switch {
|
|
||||||
case newAccessKeyId != "" && rotated && rotate:
|
|
||||||
// The new access key ID was provided, we had previously rotated
|
|
||||||
// the credentials before, and the new credential set is to be
|
|
||||||
// rotated as well. In this case, the old credentials should have
|
|
||||||
// been deleted, and the new credentials should have been rotated,
|
|
||||||
// hence, should not match the new credentials initially
|
|
||||||
// provided. Rotation time should be non-zero and updated.
|
|
||||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
||||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
|
||||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
|
||||||
require.NotZero(persistedCredsLastRotatedTime)
|
|
||||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
|
||||||
|
|
||||||
case newAccessKeyId != "" && rotated && !rotate:
|
|
||||||
// The new access key ID was provided, we had previously rotated
|
|
||||||
// the credentials before, and the new credential is *not*
|
|
||||||
// rotated. In this case, the old credentials should have
|
|
||||||
// been deleted, But the new credentials should have not been
|
|
||||||
// rotated, and hence should be the same. Rotation time should be
|
|
||||||
// zero.
|
|
||||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
||||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
|
||||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
|
||||||
require.Zero(persistedCredsLastRotatedTime)
|
|
||||||
|
|
||||||
case newAccessKeyId != "" && !rotated && rotate:
|
|
||||||
// The new access key ID was provided, we *have not* previously
|
|
||||||
// rotated the credentials, and the new credential set is to be
|
|
||||||
// rotated. In this case, the old credentials should have been
|
|
||||||
// left alone, and the new credentials should have been rotated,
|
|
||||||
// hence, should not match the new credentials initially
|
|
||||||
// provided. Rotation time should be non-zero, but updated.
|
|
||||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
||||||
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
|
||||||
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
|
||||||
require.NotZero(persistedCredsLastRotatedTime)
|
|
||||||
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
|
||||||
|
|
||||||
case newAccessKeyId != "" && !rotated && !rotate:
|
|
||||||
// The new access key ID was provided, but we have not rotated
|
|
||||||
// the credentials previously and we still don't plan on rotating
|
|
||||||
// them. In this case, the old credentials should still be valid,
|
|
||||||
// and the persisted ones should match the new ones provided.
|
|
||||||
// Rotation time should be zero.
|
|
||||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
||||||
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
|
||||||
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
|
||||||
require.Zero(persistedCredsLastRotatedTime)
|
|
||||||
|
|
||||||
case newAccessKeyId == "" && rotated && rotate:
|
|
||||||
// No new credentials have been provided, but we have previously
|
|
||||||
// rotated and are still rotating credentials. This is a no-op.
|
|
||||||
// Existing credentials should still be valid and match the ones
|
|
||||||
// persisted to state. Rotation time should be identical since
|
|
||||||
// no new rotation occurred.
|
|
||||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
||||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
|
||||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
|
||||||
require.NotZero(persistedCredsLastRotatedTime)
|
|
||||||
require.True(currentCredsLastRotatedTime.Equal(persistedCredsLastRotatedTime))
|
|
||||||
|
|
||||||
case newAccessKeyId == "" && rotated && !rotate:
|
|
||||||
// No new credentials have been provided, and we have previously
|
|
||||||
// rotated the credentials. This is actually an error, but we
|
|
||||||
// don't test it here; it's covered in unit testing (see
|
|
||||||
// plugin_test.go).
|
|
||||||
require.FailNow("testing rotated-to-not-rotated scenario not implemented by this helper")
|
|
||||||
|
|
||||||
case newAccessKeyId == "" && !rotated && rotate:
|
|
||||||
// No new credentials have been provided, and while we did not
|
|
||||||
// rotate before, we want to switch to rotation. In this case,
|
|
||||||
// the existing persisted credentials should have been rotated,
|
|
||||||
// with a new non-zero timestamp.
|
|
||||||
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
||||||
require.NotEqual(persistedAccessKeyId, currentAccessKeyId)
|
|
||||||
require.NotEqual(persistedSecretAccessKey, currentSecretAccessKey)
|
|
||||||
require.NotZero(persistedCredsLastRotatedTime)
|
|
||||||
|
|
||||||
case newAccessKeyId == "" && !rotated && !rotate:
|
|
||||||
// No new credentials have been provided and we have not, nor do
|
|
||||||
// not, plan on rotating the credentials. This is a no-op.
|
|
||||||
// Existing credentials should still be valid and match the ones
|
|
||||||
// persisted to state. Rotation time should remain at zero.
|
|
||||||
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
||||||
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
|
||||||
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
|
||||||
require.Zero(persistedCredsLastRotatedTime)
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Scenario was reached that was not covered by this function.
|
|
||||||
require.FailNow("unknown test scenario")
|
|
||||||
}
|
|
||||||
|
|
||||||
return persistedAccessKeyId, persistedSecretAccessKey
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package testing
|
package testing
|
||||||
|
|||||||
Reference in new issue
Block a user