Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8d8bae8a3 | ||
|
|
838d69d790 | ||
|
|
7ecb248c86 | ||
|
|
120ba8e8e4 | ||
|
|
77484a3fc9 | ||
|
|
1ad95dc698 | ||
|
|
e484c09479 | ||
|
|
4126a0dbfc | ||
|
|
09dd7f8a4d | ||
|
|
4c4a901d33 | ||
|
|
163ec69771 |
No files matched your search
@@ -4,6 +4,8 @@ on:
|
|||||||
- workflow_dispatch
|
- workflow_dispatch
|
||||||
- push
|
- push
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ Credential rotation can be turned off by setting the
|
|||||||
This plugin will attempt to assume a role when a `role_arn` is supplied through the
|
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).
|
`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).
|
||||||
|
|
||||||
|
To discover hosts across multiple AWS accounts, supply a `target_role_arn` in addition to `role_arn`. The plugin uses the principal credentials to assume the target role via a two-hop `AssumeRole` chain, enabling cross-account host discovery from a single worker. See [Attributes and Secrets](plugin/service/host/README.md#attributes-and-secrets) for the full list of `target_*` attributes.
|
||||||
|
|
||||||
### Environment Credentials
|
### Environment Credentials
|
||||||
|
|
||||||
This plugin will attempt to retrieve credentials from environment variables. More
|
This plugin will attempt to retrieve credentials from environment variables. More
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ go 1.25.7
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.7
|
github.com/aws/aws-sdk-go-v2 v1.41.7
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.46
|
||||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0
|
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0
|
||||||
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6
|
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0
|
||||||
@@ -13,7 +14,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/hashicorp/boundary/sdk v0.0.59
|
github.com/hashicorp/boundary/sdk v0.0.59
|
||||||
github.com/hashicorp/go-multierror v1.1.1
|
github.com/hashicorp/go-multierror v1.1.1
|
||||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.2
|
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.3
|
||||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0
|
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0
|
||||||
github.com/hashicorp/terraform-json v0.23.0
|
github.com/hashicorp/terraform-json v0.23.0
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
@@ -26,7 +27,6 @@ require (
|
|||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
|
||||||
|
|||||||
@@ -8,12 +8,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew
|
|||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
github.com/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/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
|
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0=
|
github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0=
|
||||||
@@ -22,42 +18,26 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgb
|
|||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA=
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.8 h1:1/bT9kDdLQzfZ1e6J6hpW+SfNDd6xrV8F3M2CuGyUz8=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.8/go.mod h1:RbdwTONAIi59ej/+1H+QzZORt5bcyAtbrS7FQb2pvz0=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0 h1:fTLR6dLDTGChAjecRPlVrKeznT0rVdzR4yn9Z68MTGk=
|
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0 h1:fTLR6dLDTGChAjecRPlVrKeznT0rVdzR4yn9Z68MTGk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0/go.mod h1:V0jbRy1/IPapnkqgXSwVOFB+u5pnCwd9S+R3pKWULC4=
|
github.com/aws/aws-sdk-go-v2/service/ec2 v1.254.0/go.mod h1:V0jbRy1/IPapnkqgXSwVOFB+u5pnCwd9S+R3pKWULC4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6 h1:EWehQXACWr+6hzfZPwZChlfoVhiUCfLHE0Xh3kAfzWQ=
|
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6 h1:EWehQXACWr+6hzfZPwZChlfoVhiUCfLHE0Xh3kAfzWQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6/go.mod h1:qRXgEBWPIltrWHQwU+HkyBvwh1QgeigFcaCGCIVrWk0=
|
github.com/aws/aws-sdk-go-v2/service/iam v1.47.6/go.mod h1:qRXgEBWPIltrWHQwU+HkyBvwh1QgeigFcaCGCIVrWk0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.8 h1:tIN8MFT1z5STK5kTdOT1TCfMN/bn5fSEnlKsTL8qBOU=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.8/go.mod h1:VKS56txtNWjKI8FqD/hliL0BcshyF4ZaLBa1rm2Y+5s=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 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/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 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/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 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/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 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/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 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM=
|
||||||
@@ -66,8 +46,6 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3
|
|||||||
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/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 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
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 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
|
||||||
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
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=
|
||||||
@@ -116,8 +94,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
|||||||
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.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
|
||||||
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
|
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
|
||||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.2 h1:KlDrZWx/wYpnUcFPB0s9uhPjyNZoeIfhrV2MN/Ly76s=
|
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.3 h1:mLe7IMysLSedhuu00W7AXsbfb90AA7UKfZmw3OkJ8n4=
|
||||||
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.2/go.mod h1:6+rVulOPNCQbL3Xv2iLCqM0JmU2WO2wRzP1C6hBKeB8=
|
github.com/hashicorp/go-secure-stdlib/awsutil/v2 v2.1.3/go.mod h1:6+rVulOPNCQbL3Xv2iLCqM0JmU2WO2wRzP1C6hBKeB8=
|
||||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng=
|
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 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/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
|
||||||
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.13 h1:TayxZ5drfMP0G6T++WvnLESGLOWeHtdWDWWTAi2e3Qk=
|
github.com/hashicorp/go-secure-stdlib/configutil/v2 v2.0.13 h1:TayxZ5drfMP0G6T++WvnLESGLOWeHtdWDWWTAi2e3Qk=
|
||||||
|
|||||||
@@ -186,3 +186,63 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
|
|||||||
RoleTags: roleTags,
|
RoleTags: roleTags,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTargetCredentialAttributes parses values out of a protobuf struct input and returns a CredentialAttributes used
|
||||||
|
// for configuring a second AWS session. target_role_arn is required when parsing target credentials.
|
||||||
|
// A status error is returned with an InvalidArgument code when target account attributes are missing
|
||||||
|
// required values or have invalid value types.
|
||||||
|
func GetTargetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error) {
|
||||||
|
badFields := make(map[string]string)
|
||||||
|
|
||||||
|
// Parse the role ARN of the target account to AssumeRole into.
|
||||||
|
targetRoleArn, err := values.GetStringValue(in, ConstTargetRoleArn, true)
|
||||||
|
if err != nil {
|
||||||
|
badFields[fmt.Sprintf("attributes.%s", ConstTargetRoleArn)] = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// try parsing target_region, fallback to region, adds target_region to bad fields only when malformed since not required
|
||||||
|
targetRegion, err := values.GetStringValue(in, ConstTargetRegion, false)
|
||||||
|
if err != nil {
|
||||||
|
badFields[fmt.Sprintf("attributes.%s", ConstTargetRegion)] = err.Error()
|
||||||
|
}
|
||||||
|
if targetRegion == "" {
|
||||||
|
targetRegion, err = values.GetStringValue(in, ConstRegion, true) // region is required for processing
|
||||||
|
if err != nil {
|
||||||
|
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the remaining fields are optional and only raise errors when malformed
|
||||||
|
disableCredentialRotation, err := values.GetBoolValue(in, ConstDisableCredentialRotation, false)
|
||||||
|
if err != nil {
|
||||||
|
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRoleExternalId, err := values.GetStringValue(in, ConstTargetRoleExternalId, false)
|
||||||
|
if err != nil {
|
||||||
|
badFields[fmt.Sprintf("attributes.%s", ConstTargetRoleExternalId)] = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRoleSessionName, err := values.GetStringValue(in, ConstTargetRoleSessionName, false)
|
||||||
|
if err != nil {
|
||||||
|
badFields[fmt.Sprintf("attributes.%s", ConstTargetRoleSessionName)] = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRoleTags, err := values.GetMapStringString(in, ConstTargetRoleTags, false)
|
||||||
|
if err != nil {
|
||||||
|
badFields[fmt.Sprintf("attributes.%s", ConstTargetRoleTags)] = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(badFields) > 0 {
|
||||||
|
return nil, errors.InvalidArgumentError("Error in the attributes provided", badFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CredentialAttributes{
|
||||||
|
Region: targetRegion,
|
||||||
|
DisableCredentialRotation: disableCredentialRotation,
|
||||||
|
RoleArn: targetRoleArn,
|
||||||
|
RoleExternalId: targetRoleExternalId,
|
||||||
|
RoleSessionName: targetRoleSessionName,
|
||||||
|
RoleTags: targetRoleTags,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -101,6 +101,232 @@ func TestGetCredentialAttributes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetTargetCredentialAttributes(t *testing.T) {
|
||||||
|
const targetRoleArn = "arn:aws:iam::222222222222:role/Target"
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
in map[string]any
|
||||||
|
expected *CredentialAttributes
|
||||||
|
expectedErrContains string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing target_role_arn",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing target_role_arn and region",
|
||||||
|
in: map[string]any{},
|
||||||
|
expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty target_role_arn",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRoleArn: "",
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_arn: value \"target_role_arn\" cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong type target_role_arn",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRoleArn: true,
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_arn: unexpected type for value \"target_role_arn\": want string, got bool",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target_region without target_role_arn",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRegion: "eu-west-1",
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other target fields without target_role_arn",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRoleExternalId: "ext",
|
||||||
|
ConstTargetRoleSessionName: "sess",
|
||||||
|
ConstTargetRoleTags: map[string]any{"k": "v"},
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target_role_arn uses region when target_region omitted",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
},
|
||||||
|
expected: &CredentialAttributes{
|
||||||
|
Region: "us-west-2",
|
||||||
|
RoleArn: targetRoleArn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target_role_arn without target_region or region",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
},
|
||||||
|
expectedErrContains: "missing required value \"region\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target_region overrides region",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRegion: "eu-west-1",
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
},
|
||||||
|
expected: &CredentialAttributes{
|
||||||
|
Region: "eu-west-1",
|
||||||
|
RoleArn: targetRoleArn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty target_region falls back to region",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRegion: "",
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
},
|
||||||
|
expected: &CredentialAttributes{
|
||||||
|
Region: "us-west-2",
|
||||||
|
RoleArn: targetRoleArn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all optional target fields",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstDisableCredentialRotation: true,
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
ConstTargetRegion: "eu-west-1",
|
||||||
|
ConstTargetRoleExternalId: "target-ext",
|
||||||
|
ConstTargetRoleSessionName: "target-sess",
|
||||||
|
ConstTargetRoleTags: map[string]any{"env": "target"},
|
||||||
|
},
|
||||||
|
expected: &CredentialAttributes{
|
||||||
|
Region: "eu-west-1",
|
||||||
|
DisableCredentialRotation: true,
|
||||||
|
RoleArn: targetRoleArn,
|
||||||
|
RoleExternalId: "target-ext",
|
||||||
|
RoleSessionName: "target-sess",
|
||||||
|
RoleTags: map[string]string{"env": "target"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "optional target fields omitted do not copy principal role fields",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstRoleArn: "arn:aws:iam::111111111111:role/Principal",
|
||||||
|
ConstRoleExternalId: "principal-ext",
|
||||||
|
ConstRoleSessionName: "principal-sess",
|
||||||
|
ConstRoleTags: map[string]any{"env": "principal"},
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
},
|
||||||
|
expected: &CredentialAttributes{
|
||||||
|
Region: "us-west-2",
|
||||||
|
RoleArn: targetRoleArn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disable_credential_rotation false is copied",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstDisableCredentialRotation: false,
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
},
|
||||||
|
expected: &CredentialAttributes{
|
||||||
|
Region: "us-west-2",
|
||||||
|
DisableCredentialRotation: false,
|
||||||
|
RoleArn: targetRoleArn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong type target_region",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
ConstTargetRegion: true,
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_region: unexpected type for value \"target_region\": want string, got bool",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong type target_role_external_id",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
ConstTargetRoleExternalId: true,
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_external_id: unexpected type for value \"target_role_external_id\": want string, got bool",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong type target_role_session_name",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
ConstTargetRoleSessionName: true,
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_session_name: unexpected type for value \"target_role_session_name\": want string, got bool",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong type target_role_tags",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
ConstTargetRoleTags: true,
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_tags: unexpected type for value \"target_role_tags\": want map[string]string, got bool",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong type target_role_tag value",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
ConstTargetRoleTags: map[string]any{
|
||||||
|
"env": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrContains: `unexpected type for value in map["env"]: want string, got bool`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong type disable_credential_rotation",
|
||||||
|
in: map[string]any{
|
||||||
|
ConstRegion: "us-west-2",
|
||||||
|
ConstTargetRoleArn: targetRoleArn,
|
||||||
|
ConstDisableCredentialRotation: "sure",
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.disable_credential_rotation: unexpected type for value \"disable_credential_rotation\": want bool, got string",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
input, err := structpb.NewStruct(tc.in)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
actual, err := GetTargetCredentialAttributes(input)
|
||||||
|
if tc.expectedErrContains != "" {
|
||||||
|
require.Error(err)
|
||||||
|
require.Nil(actual)
|
||||||
|
require.Contains(err.Error(), tc.expectedErrContains)
|
||||||
|
require.Equal(codes.InvalidArgument, status.Code(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(tc.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetCredentialsConfig(t *testing.T) {
|
func TestGetCredentialsConfig(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
package credential
|
package credential
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ConstAccessKey is the key for the region in the aws credentials.
|
// ConstRegion is the key for the region in the aws credentials.
|
||||||
ConstRegion = "region"
|
ConstRegion = "region"
|
||||||
|
|
||||||
// ConstAccessKey is the key for the access key id in the aws credentials.
|
// ConstAccessKeyId is the key for the access key id in the aws credentials.
|
||||||
ConstAccessKeyId = "access_key_id"
|
ConstAccessKeyId = "access_key_id"
|
||||||
|
|
||||||
// ConstSecretAccessKey is the key for the secret access key in the aws credentials.
|
// ConstSecretAccessKey is the key for the secret access key in the aws credentials.
|
||||||
@@ -30,4 +30,19 @@ const (
|
|||||||
|
|
||||||
// ConstRoleTags is the key for the tags used for assuming a IAM role.
|
// ConstRoleTags is the key for the tags used for assuming a IAM role.
|
||||||
ConstRoleTags = "role_tags"
|
ConstRoleTags = "role_tags"
|
||||||
|
|
||||||
|
// ConstTargetRegion is the key for the region of the target account in the aws credentials.
|
||||||
|
ConstTargetRegion = "target_region"
|
||||||
|
|
||||||
|
// ConstTargetRoleArn is the key for assuming a IAM role for the target account.
|
||||||
|
ConstTargetRoleArn = "target_role_arn"
|
||||||
|
|
||||||
|
// ConstTargetRoleExternalId is the key for the external id used for assuming a IAM role for the target account.
|
||||||
|
ConstTargetRoleExternalId = "target_role_external_id"
|
||||||
|
|
||||||
|
// ConstTargetRoleSessionName is the key for the session name used for assuming a IAM role of the target account.
|
||||||
|
ConstTargetRoleSessionName = "target_role_session_name"
|
||||||
|
|
||||||
|
// ConstTargetRoleTags is the key for the tags used for assuming a IAM role of the target account.
|
||||||
|
ConstTargetRoleTags = "target_role_tags"
|
||||||
)
|
)
|
||||||
@@ -112,10 +112,35 @@ func NewAwsCredentialPersistedState(opts ...AwsCredentialPersistedStateOption) (
|
|||||||
// 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. This method returns a status error
|
||||||
// with PluginError details.
|
// with PluginError details.
|
||||||
|
//
|
||||||
|
// When RoleARN is set and no static key is present (ambient role path),
|
||||||
|
// credentials are eagerly retrieved before calling GetCallerIdentity. This
|
||||||
|
// forces sts:AssumeRole to be called immediately so that any role errors
|
||||||
|
// (e.g. no such role, access denied) surface here rather than being deferred
|
||||||
|
// to the first real AWS API call.
|
||||||
func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
|
func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
|
||||||
if s.CredentialsConfig == nil {
|
if s.CredentialsConfig == nil {
|
||||||
return errors.BadRequestStatus("missing credentials config")
|
return errors.BadRequestStatus("missing credentials config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For the ambient role path (RoleARN set, no static key), eagerly resolve
|
||||||
|
// the credential chain so that sts:AssumeRole is called now. Without this,
|
||||||
|
// the AssumeRoleProvider inside the chain is lazy and GetCallerIdentity
|
||||||
|
// would succeed even with an invalid role ARN (it uses the mock/ambient
|
||||||
|
// identity directly rather than the assumed one).
|
||||||
|
cc := s.CredentialsConfig
|
||||||
|
if cc.RoleARN != "" && cc.AccessKey == "" {
|
||||||
|
cfg, err := s.GenerateCredentialChain(ctx)
|
||||||
|
if err != nil {
|
||||||
|
st, _ := errors.ParseAWSError("validating credentials", err)
|
||||||
|
return st.Err()
|
||||||
|
}
|
||||||
|
if _, err := cfg.Credentials.Retrieve(ctx); err != nil {
|
||||||
|
st, _ := errors.ParseAWSError("validating credentials", err)
|
||||||
|
return st.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := s.CredentialsConfig.GetCallerIdentity(ctx, s.testOpts...); err != nil {
|
if _, err := s.CredentialsConfig.GetCallerIdentity(ctx, s.testOpts...); err != nil {
|
||||||
st, _ := errors.ParseAWSError("validating credentials", err)
|
st, _ := errors.ParseAWSError("validating credentials", err)
|
||||||
return st.Err()
|
return st.Err()
|
||||||
|
|||||||
@@ -109,13 +109,23 @@ The following `attributes` are valid on an AWS host catalog resource:
|
|||||||
- `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
|
- `role_arn` (string): The AWS Role ARN used for `AssumeRole` authentication. If
|
||||||
provided, `disable_credential_rotation` must be also set to `true.
|
provided, `disable_credential_rotation` must be also set to `true`.
|
||||||
- `role_external_id` (string): The external id configured for the `AssumeRole`
|
- `role_external_id` (string): The external ID configured for the principal `AssumeRole`
|
||||||
provider.
|
provider.
|
||||||
- `role_session_name` (string): The session name configured for the `AssumeRole`
|
- `role_session_name` (string): The session name configured for the principal `AssumeRole`
|
||||||
provider.
|
provider.
|
||||||
- `role_tags` (object): The key-value pair tags configured for the `AssumeRole`
|
- `role_tags` (object): The key-value pair tags configured for the principal `AssumeRole`
|
||||||
provider.
|
provider.
|
||||||
|
- `target_role_arn` (string): The AWS role ARN of the target IAM role to assume for
|
||||||
|
cross-account discovery. Required if any other `target_*` attributes are configured.
|
||||||
|
- `target_region` (string): The region where the target hosts reside. Defaults to the
|
||||||
|
principal account `region` if omitted.
|
||||||
|
- `target_role_external_id` (string): The external ID configured for the target
|
||||||
|
`AssumeRole` provider.
|
||||||
|
- `target_role_session_name` (string): The session name configured for the target
|
||||||
|
`AssumeRole` provider.
|
||||||
|
- `target_role_tags` (object): The key-value pair tags configured for the target
|
||||||
|
`AssumeRole` provider.
|
||||||
|
|
||||||
The following `secrets` are required on an AWS host catalog resource:
|
The following `secrets` are required on an AWS host catalog resource:
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import (
|
|||||||
type CatalogAttributes struct {
|
type CatalogAttributes struct {
|
||||||
*cred.CredentialAttributes
|
*cred.CredentialAttributes
|
||||||
|
|
||||||
|
// TargetAccount optionally holds the credentials used to AssumeRole into a second, affiliated account when provided.
|
||||||
|
TargetAccount *cred.CredentialAttributes
|
||||||
|
|
||||||
// DualStack is used for configuring how the aws client will resolve requests.
|
// DualStack is used for configuring how the aws client will resolve requests.
|
||||||
DualStack bool
|
DualStack bool
|
||||||
|
|
||||||
@@ -39,6 +42,14 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var targetCredAttributes *cred.CredentialAttributes
|
||||||
|
if hasTargetAccountAttributes(in) {
|
||||||
|
targetCredAttributes, err = cred.GetTargetCredentialAttributes(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dualStack, err := values.GetBoolValue(in, ConstAwsDualStack, false)
|
dualStack, err := values.GetBoolValue(in, ConstAwsDualStack, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
badFields[fmt.Sprintf("attributes.%s", ConstAwsDualStack)] = err.Error()
|
badFields[fmt.Sprintf("attributes.%s", ConstAwsDualStack)] = err.Error()
|
||||||
@@ -66,6 +77,16 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
|||||||
continue
|
continue
|
||||||
case cred.ConstRoleTags:
|
case cred.ConstRoleTags:
|
||||||
continue
|
continue
|
||||||
|
case cred.ConstTargetRegion:
|
||||||
|
continue
|
||||||
|
case cred.ConstTargetRoleArn:
|
||||||
|
continue
|
||||||
|
case cred.ConstTargetRoleExternalId:
|
||||||
|
continue
|
||||||
|
case cred.ConstTargetRoleSessionName:
|
||||||
|
continue
|
||||||
|
case cred.ConstTargetRoleTags:
|
||||||
|
continue
|
||||||
default:
|
default:
|
||||||
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
||||||
}
|
}
|
||||||
@@ -77,6 +98,7 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
|||||||
|
|
||||||
return &CatalogAttributes{
|
return &CatalogAttributes{
|
||||||
CredentialAttributes: credAttributes,
|
CredentialAttributes: credAttributes,
|
||||||
|
TargetAccount: targetCredAttributes,
|
||||||
DualStack: dualStack,
|
DualStack: dualStack,
|
||||||
InstanceAddressesOnly: instanceAddressesOnly,
|
InstanceAddressesOnly: instanceAddressesOnly,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -121,3 +143,19 @@ func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
|
|||||||
|
|
||||||
return &setAttrs, nil
|
return &setAttrs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasTargetAccountAttributes(in *structpb.Struct) bool {
|
||||||
|
fields := in.GetFields()
|
||||||
|
|
||||||
|
_, hasTargetRegion := fields[cred.ConstTargetRegion]
|
||||||
|
_, hasTargetRoleArn := fields[cred.ConstTargetRoleArn]
|
||||||
|
_, hasTargetRoleExternalID := fields[cred.ConstTargetRoleExternalId]
|
||||||
|
_, hasTargetRoleSessionName := fields[cred.ConstTargetRoleSessionName]
|
||||||
|
_, hasTargetRoleTags := fields[cred.ConstTargetRoleTags]
|
||||||
|
|
||||||
|
return hasTargetRegion ||
|
||||||
|
hasTargetRoleArn ||
|
||||||
|
hasTargetRoleExternalID ||
|
||||||
|
hasTargetRoleSessionName ||
|
||||||
|
hasTargetRoleTags
|
||||||
|
}
|
||||||
@@ -118,6 +118,170 @@ func TestGetCatalogAttributes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "target_region without target_role_arn",
|
||||||
|
in: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
"region": structpb.NewStringValue("us-west-2"),
|
||||||
|
"target_region": structpb.NewStringValue("eu-west-1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target role field without target_role_arn",
|
||||||
|
in: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
"region": structpb.NewStringValue("us-west-2"),
|
||||||
|
"target_role_external_id": structpb.NewStringValue("target-ext"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other target role fields without target_role_arn",
|
||||||
|
in: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
"region": structpb.NewStringValue("us-west-2"),
|
||||||
|
"target_role_session_name": structpb.NewStringValue("target-session"),
|
||||||
|
"target_role_tags": structpb.NewStructValue(&structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
"env": structpb.NewStringValue("target"),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty target_role_arn with target_region",
|
||||||
|
in: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
"region": structpb.NewStringValue("us-west-2"),
|
||||||
|
"target_role_arn": structpb.NewStringValue(""),
|
||||||
|
"target_region": structpb.NewStringValue("eu-west-1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_arn: value \"target_role_arn\" cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target_role_arn without target_region uses region; principal sts fields are not copied",
|
||||||
|
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::111111111111:role/Principal"),
|
||||||
|
"role_external_id": structpb.NewStringValue("principal-ext"),
|
||||||
|
"role_session_name": structpb.NewStringValue("principal-sess"),
|
||||||
|
"role_tags": structpb.NewStructValue(&structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
"env": structpb.NewStringValue("principal"),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
"target_role_arn": structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &CatalogAttributes{
|
||||||
|
CredentialAttributes: &cred.CredentialAttributes{
|
||||||
|
Region: "us-west-2",
|
||||||
|
DisableCredentialRotation: true,
|
||||||
|
RoleArn: "arn:aws:iam::111111111111:role/Principal",
|
||||||
|
RoleExternalId: "principal-ext",
|
||||||
|
RoleSessionName: "principal-sess",
|
||||||
|
RoleTags: map[string]string{
|
||||||
|
"env": "principal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TargetAccount: &cred.CredentialAttributes{
|
||||||
|
Region: "us-west-2",
|
||||||
|
DisableCredentialRotation: true,
|
||||||
|
RoleArn: "arn:aws:iam::222222222222:role/Target",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target attributes are independent of principal",
|
||||||
|
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::111111111111:role/Principal"),
|
||||||
|
"role_external_id": structpb.NewStringValue("principal-ext"),
|
||||||
|
"role_session_name": structpb.NewStringValue("principal-sess"),
|
||||||
|
"role_tags": structpb.NewStructValue(&structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
"env": structpb.NewStringValue("principal"),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
"target_role_arn": structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
|
||||||
|
"target_region": structpb.NewStringValue("eu-west-1"),
|
||||||
|
"target_role_external_id": structpb.NewStringValue("target-ext"),
|
||||||
|
"target_role_session_name": structpb.NewStringValue("target-sess"),
|
||||||
|
"target_role_tags": structpb.NewStructValue(&structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
"env": structpb.NewStringValue("target"),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &CatalogAttributes{
|
||||||
|
CredentialAttributes: &cred.CredentialAttributes{
|
||||||
|
Region: "us-west-2",
|
||||||
|
DisableCredentialRotation: true,
|
||||||
|
RoleArn: "arn:aws:iam::111111111111:role/Principal",
|
||||||
|
RoleExternalId: "principal-ext",
|
||||||
|
RoleSessionName: "principal-sess",
|
||||||
|
RoleTags: map[string]string{
|
||||||
|
"env": "principal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TargetAccount: &cred.CredentialAttributes{
|
||||||
|
Region: "eu-west-1",
|
||||||
|
DisableCredentialRotation: true,
|
||||||
|
RoleArn: "arn:aws:iam::222222222222:role/Target",
|
||||||
|
RoleExternalId: "target-ext",
|
||||||
|
RoleSessionName: "target-sess",
|
||||||
|
RoleTags: map[string]string{
|
||||||
|
"env": "target",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "catalog dual_stack and instance_addresses_only with target",
|
||||||
|
in: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
"region": structpb.NewStringValue("us-west-2"),
|
||||||
|
"dual_stack": structpb.NewBoolValue(true),
|
||||||
|
"instance_addresses_only": structpb.NewBoolValue(true),
|
||||||
|
"target_role_arn": structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &CatalogAttributes{
|
||||||
|
CredentialAttributes: &credential.CredentialAttributes{
|
||||||
|
Region: "us-west-2",
|
||||||
|
DisableCredentialRotation: false,
|
||||||
|
},
|
||||||
|
TargetAccount: &cred.CredentialAttributes{
|
||||||
|
Region: "us-west-2",
|
||||||
|
DisableCredentialRotation: false,
|
||||||
|
RoleArn: "arn:aws:iam::222222222222:role/Target",
|
||||||
|
},
|
||||||
|
DualStack: true,
|
||||||
|
InstanceAddressesOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown field with target_role_arn",
|
||||||
|
in: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
"region": structpb.NewStringValue("us-west-2"),
|
||||||
|
"target_role_arn": structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
|
||||||
|
"foo": structpb.NewBoolValue(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.foo: unrecognized field",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
|
|||||||
+194
-51
@@ -5,12 +5,17 @@ package host
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
goerrors "errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||||
|
stsTypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
|
||||||
|
"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-plugin-aws/internal/errors"
|
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
@@ -32,6 +37,18 @@ type HostPlugin struct {
|
|||||||
testCatalogStateOpts []awsCatalogPersistedStateOption
|
testCatalogStateOpts []awsCatalogPersistedStateOption
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hostSetQuery struct {
|
||||||
|
Id string
|
||||||
|
Input *ec2.DescribeInstancesInput
|
||||||
|
Output *ec2.DescribeInstancesOutput
|
||||||
|
OutputHosts []*pb.ListHostsResponseHost
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSessionName = "boundary-default"
|
||||||
|
const dryRunOperationErrorCode = "DryRunOperation"
|
||||||
|
const errBuildDescribeInstancesInput = "error building DescribeInstances input: %s"
|
||||||
|
const errDryRunFailed = "error performing DescribeInstances dry run: %s"
|
||||||
|
|
||||||
// Ensure that we are implementing HostPluginServiceServer
|
// Ensure that we are implementing HostPluginServiceServer
|
||||||
var _ pb.HostPluginServiceServer = (*HostPlugin)(nil)
|
var _ pb.HostPluginServiceServer = (*HostPlugin)(nil)
|
||||||
|
|
||||||
@@ -73,6 +90,15 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate assumed-role credentials at catalog-creation time so that an
|
||||||
|
// invalid role_arn is rejected immediately rather than silently falling
|
||||||
|
// back to ambient credentials.
|
||||||
|
if credential.GetCredentialType(credState.CredentialsConfig) == credential.DynamicAWS {
|
||||||
|
if err := credState.ValidateCreds(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
catalogState, err := newAwsCatalogPersistedState(
|
catalogState, err := newAwsCatalogPersistedState(
|
||||||
append([]awsCatalogPersistedStateOption{
|
append([]awsCatalogPersistedStateOption{
|
||||||
withCredentials(credState),
|
withCredentials(credState),
|
||||||
@@ -82,13 +108,18 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
|||||||
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
|
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// perform dry run to ensure we can interact with AWS as expected.
|
|
||||||
opts := []ec2Option{}
|
opts := []ec2Option{}
|
||||||
if catalogAttributes.DualStack {
|
if catalogAttributes.DualStack {
|
||||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
||||||
}
|
}
|
||||||
if st := dryRunValidation(ctx, catalogState, opts); st != nil {
|
input, err := buildDescribeInstancesInput(&SetAttributes{}, true)
|
||||||
return nil, st.Err()
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Unknown, errBuildDescribeInstancesInput, err)
|
||||||
|
}
|
||||||
|
// Run a DescribeInstances request with DryRun set to true to ensure
|
||||||
|
// we can interact with AWS resources as expected.
|
||||||
|
if _, err := checkHosts(ctx, catalogState, input, opts, catalogAttributes.TargetAccount); err != nil {
|
||||||
|
return nil, fmt.Errorf(errDryRunFailed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
persistedProto, err := catalogState.toProto()
|
persistedProto, err := catalogState.toProto()
|
||||||
@@ -163,6 +194,15 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error setting up new credential persisted state: %s", err)
|
return nil, errors.BadRequestStatusf("error setting up new credential persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate assumed-role credentials when the role_arn changes so that
|
||||||
|
// an invalid ARN is caught before replacing the existing credential state.
|
||||||
|
if credential.GetCredentialType(newCredState.CredentialsConfig) == credential.DynamicAWS {
|
||||||
|
if err := newCredState.ValidateCreds(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
newCatalogState, err := newAwsCatalogPersistedState(
|
newCatalogState, err := newAwsCatalogPersistedState(
|
||||||
append([]awsCatalogPersistedStateOption{
|
append([]awsCatalogPersistedStateOption{
|
||||||
withCredentials(newCredState),
|
withCredentials(newCredState),
|
||||||
@@ -171,12 +211,19 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := []ec2Option{}
|
opts := []ec2Option{}
|
||||||
if newCatalogAttributes.DualStack {
|
if newCatalogAttributes.DualStack {
|
||||||
opts = append(opts, WithDualStack(newCatalogAttributes.DualStack))
|
opts = append(opts, WithDualStack(newCatalogAttributes.DualStack))
|
||||||
}
|
}
|
||||||
if st := dryRunValidation(ctx, newCatalogState, opts); st != nil {
|
input, err := buildDescribeInstancesInput(&SetAttributes{}, true)
|
||||||
return nil, st.Err()
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Unknown, errBuildDescribeInstancesInput, err)
|
||||||
|
}
|
||||||
|
// Run a DescribeInstances request with DryRun set to true to ensure
|
||||||
|
// we can interact with AWS resources as expected.
|
||||||
|
if _, err := checkHosts(ctx, newCatalogState, input, opts, newCatalogAttributes.TargetAccount); err != nil {
|
||||||
|
return nil, fmt.Errorf(errDryRunFailed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace the existing credential state.
|
// Replace the existing credential state.
|
||||||
@@ -216,13 +263,18 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
|||||||
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// perform dry run to ensure we can interact with AWS as expected.
|
|
||||||
opts := []ec2Option{}
|
opts := []ec2Option{}
|
||||||
if newCatalogAttributes.DualStack {
|
if newCatalogAttributes.DualStack {
|
||||||
opts = append(opts, WithDualStack(newCatalogAttributes.DualStack))
|
opts = append(opts, WithDualStack(newCatalogAttributes.DualStack))
|
||||||
}
|
}
|
||||||
if st := dryRunValidation(ctx, catalogState, opts); st != nil {
|
input, err := buildDescribeInstancesInput(&SetAttributes{}, true)
|
||||||
return nil, st.Err()
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Unknown, errBuildDescribeInstancesInput, err)
|
||||||
|
}
|
||||||
|
// Run a DescribeInstances request with DryRun set to true to ensure
|
||||||
|
// we can interact with AWS resources as expected.
|
||||||
|
if _, err := checkHosts(ctx, catalogState, input, opts, newCatalogAttributes.TargetAccount); err != nil {
|
||||||
|
return nil, fmt.Errorf(errDryRunFailed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
persistedProto, err := catalogState.toProto()
|
persistedProto, err := catalogState.toProto()
|
||||||
@@ -366,18 +418,20 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
describeInstanceFilters, err := buildFilters(setAttrs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.BadRequestStatusf("error building set filters: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := []ec2Option{}
|
opts := []ec2Option{}
|
||||||
if catalogAttributes.DualStack {
|
if catalogAttributes.DualStack {
|
||||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
||||||
}
|
}
|
||||||
if st := dryRunValidation(ctx, catalogState, opts, describeInstanceFilters...); st != nil {
|
input, err := buildDescribeInstancesInput(setAttrs, true)
|
||||||
return nil, st.Err()
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Unknown, errBuildDescribeInstancesInput, err)
|
||||||
}
|
}
|
||||||
|
// Run a DescribeInstances request with DryRun set to true to ensure
|
||||||
|
// we can interact with AWS resources as expected.
|
||||||
|
if _, err := checkHosts(ctx, catalogState, input, opts, catalogAttributes.TargetAccount); err != nil {
|
||||||
|
return nil, fmt.Errorf(errDryRunFailed, err)
|
||||||
|
}
|
||||||
|
|
||||||
return &pb.OnCreateSetResponse{}, nil
|
return &pb.OnCreateSetResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,17 +485,18 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
describeInstanceFilters, err := buildFilters(setAttrs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.BadRequestStatusf("error building set filters: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := []ec2Option{}
|
opts := []ec2Option{}
|
||||||
if catalogAttributes.DualStack {
|
if catalogAttributes.DualStack {
|
||||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
||||||
}
|
}
|
||||||
if st := dryRunValidation(ctx, catalogState, opts, describeInstanceFilters...); st != nil {
|
input, err := buildDescribeInstancesInput(setAttrs, true)
|
||||||
return nil, st.Err()
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Unknown, errBuildDescribeInstancesInput, err)
|
||||||
|
}
|
||||||
|
// Run a DescribeInstances request with DryRun set to true to ensure
|
||||||
|
// we can interact with AWS resources as expected.
|
||||||
|
if _, err := checkHosts(ctx, catalogState, input, opts, catalogAttributes.TargetAccount); err != nil {
|
||||||
|
return nil, fmt.Errorf(errDryRunFailed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &pb.OnUpdateSetResponse{}, nil
|
return &pb.OnUpdateSetResponse{}, nil
|
||||||
@@ -495,13 +550,6 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build all the queries in advance.
|
// Build all the queries in advance.
|
||||||
type hostSetQuery struct {
|
|
||||||
Id string
|
|
||||||
Input *ec2.DescribeInstancesInput
|
|
||||||
Output *ec2.DescribeInstancesOutput
|
|
||||||
OutputHosts []*pb.ListHostsResponseHost
|
|
||||||
}
|
|
||||||
|
|
||||||
queries := make([]hostSetQuery, len(sets))
|
queries := make([]hostSetQuery, len(sets))
|
||||||
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
|
||||||
@@ -531,20 +579,22 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
|||||||
if catalogAttributes.DualStack {
|
if catalogAttributes.DualStack {
|
||||||
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
||||||
}
|
}
|
||||||
ec2Client, err := catalogState.EC2Client(ctx, opts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.BadRequestStatusf("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 := checkHosts(ctx, catalogState, query.Input, opts, catalogAttributes.TargetAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.BadRequestStatusf("error running DescribeInstances for host set id %q: %s", query.Id, err)
|
if catalogAttributes.TargetAccount != nil {
|
||||||
|
return nil, errors.BadRequestStatusf("unable to list hosts for host set %q: %s", query.Id, err)
|
||||||
|
}
|
||||||
|
return nil, errors.BadRequestStatusf("error retrieving host results for host set id %q: %s", query.Id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
queries[i].Output = output
|
queries[i].Output = output
|
||||||
|
if output == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Process the output here, we will normalize this into a single
|
// Process the output here, we will normalize this into a single
|
||||||
// set of hosts afterwards (possibly removing duplicates).
|
// set of hosts afterwards (possibly removing duplicates).
|
||||||
@@ -601,11 +651,11 @@ func buildFilters(attrs *SetAttributes) ([]types.Filter, error) {
|
|||||||
splitFilter := strings.Split(filterAttr, "=")
|
splitFilter := strings.Split(filterAttr, "=")
|
||||||
switch {
|
switch {
|
||||||
case len(splitFilter) != 2:
|
case len(splitFilter) != 2:
|
||||||
return nil, fmt.Errorf("expected filter %q to contain a single equal sign", filterAttr)
|
return nil, errors.BadRequestStatusf("expected filter %q to contain a single equal sign", filterAttr)
|
||||||
case len(splitFilter[0]) == 0:
|
case len(splitFilter[0]) == 0:
|
||||||
return nil, fmt.Errorf("filter %q contains an empty filter key", filterAttr)
|
return nil, errors.BadRequestStatusf("filter %q contains an empty filter key", filterAttr)
|
||||||
case len(splitFilter[1]) == 0:
|
case len(splitFilter[1]) == 0:
|
||||||
return nil, fmt.Errorf("filter %q contains an empty value", filterAttr)
|
return nil, errors.BadRequestStatusf("filter %q contains an empty value", filterAttr)
|
||||||
}
|
}
|
||||||
|
|
||||||
filterKey, filterValue := splitFilter[0], splitFilter[1]
|
filterKey, filterValue := splitFilter[0], splitFilter[1]
|
||||||
@@ -634,9 +684,13 @@ func buildFilters(attrs *SetAttributes) ([]types.Filter, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.DescribeInstancesInput, error) {
|
func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.DescribeInstancesInput, error) {
|
||||||
|
if attrs == nil {
|
||||||
|
attrs = &SetAttributes{}
|
||||||
|
}
|
||||||
|
|
||||||
filters, err := buildFilters(attrs)
|
filters, err := buildFilters(attrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error building filters: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ec2.DescribeInstancesInput{
|
return &ec2.DescribeInstancesInput{
|
||||||
@@ -707,26 +761,115 @@ func awsInstanceToHost(instance types.Instance, catalogAttributes *CatalogAttrib
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// dryRunValidation performs an AWS DescribeInstances call to verify the state's
|
func checkHosts(ctx context.Context,
|
||||||
// credentials, the host listing functionality as well as the filters, if any
|
principalCatalogState *awsCatalogPersistedState,
|
||||||
// are passed in. This function can therefore be used for both host catalog and
|
input *ec2.DescribeInstancesInput,
|
||||||
// host set validation.
|
opts []ec2Option,
|
||||||
func dryRunValidation(ctx context.Context, state *awsCatalogPersistedState, ec2Opts []ec2Option, filters ...types.Filter) *status.Status {
|
target *credential.CredentialAttributes,
|
||||||
if state == nil {
|
) (*ec2.DescribeInstancesOutput, error) {
|
||||||
return status.New(codes.InvalidArgument, "persisted state is required")
|
if principalCatalogState == nil {
|
||||||
|
return nil, fmt.Errorf("unable to create EC2 client from catalog state")
|
||||||
}
|
}
|
||||||
|
|
||||||
ec2Client, err := state.EC2Client(ctx, ec2Opts...)
|
var ec2Client EC2API
|
||||||
|
var err error
|
||||||
|
if target == nil {
|
||||||
|
// use the principal account to execute the DescribeInstances command
|
||||||
|
ec2Client, err = principalCatalogState.EC2Client(ctx, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// use the target account to execute the DescribeInstances command
|
||||||
|
ec2Client, err = ec2ClientForTarget(ctx, principalCatalogState, target, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("EC2 client setup failed for target account: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := ec2Client.DescribeInstances(ctx, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.New(codes.InvalidArgument, fmt.Sprintf("error getting EC2 client: %s", err))
|
var awsErr smithy.APIError
|
||||||
|
ok := goerrors.As(err, &awsErr)
|
||||||
|
if ok && awsErr.ErrorCode() == dryRunOperationErrorCode {
|
||||||
|
// successful DryRun validation, no response necessary, no error thrown
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if target != nil {
|
||||||
|
return nil, fmt.Errorf("EC2 DescribeInstances failed for target account: %w", err)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = ec2Client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{Filters: filters})
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ec2ClientForTarget builds an EC2 client to query the target account.
|
||||||
|
// The hop is a continuation of the principal session: STS AssumeRole is called
|
||||||
|
// as the principal to get to the target, then EC2 uses the resulting temporary credentials.
|
||||||
|
// This is not a second worker-level credential chain on the target attributes.
|
||||||
|
func ec2ClientForTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
principalCatalogState *awsCatalogPersistedState,
|
||||||
|
target *credential.CredentialAttributes,
|
||||||
|
opts []ec2Option) (EC2API, error) {
|
||||||
|
// Reuse the principal SDK config (HTTP client, retries, logger, and the
|
||||||
|
// principal identity).
|
||||||
|
principalCfg, err := principalCatalogState.GenerateCredentialChain(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.New(codes.FailedPrecondition, fmt.Sprintf("aws describe instances failed: %s", err))
|
return nil, fmt.Errorf("principal AWS configuration could not be loaded: %w", err)
|
||||||
|
}
|
||||||
|
if principalCfg == nil {
|
||||||
|
return nil, fmt.Errorf("nil principal AWS configuration while configuring EC2 client for target account")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// STS must run as the principal so the target role's trust policy sees that
|
||||||
|
// identity, not the worker's. Session name, external ID, and tags are
|
||||||
|
// forwarded from attrs when set so the AssumeRole matches the target role's configuration.
|
||||||
|
stsClient := sts.NewFromConfig(*principalCfg)
|
||||||
|
provider := stscreds.NewAssumeRoleProvider(stsClient, target.RoleArn, func(o *stscreds.AssumeRoleOptions) {
|
||||||
|
if target.RoleSessionName != "" {
|
||||||
|
o.RoleSessionName = target.RoleSessionName
|
||||||
|
} else {
|
||||||
|
// STS may reject the AssumeRole request if session name is not provided.
|
||||||
|
o.RoleSessionName = defaultSessionName
|
||||||
|
}
|
||||||
|
if target.RoleExternalId != "" {
|
||||||
|
o.ExternalID = &target.RoleExternalId
|
||||||
|
}
|
||||||
|
for k, v := range target.RoleTags {
|
||||||
|
o.Tags = append(o.Tags, stsTypes.Tag{
|
||||||
|
Key: aws.String(k),
|
||||||
|
Value: aws.String(v),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Same worker SDK environment as the principal; only identity and region change.
|
||||||
|
targetCfg := principalCfg.Copy()
|
||||||
|
targetCfg.Credentials = aws.NewCredentialsCache(provider)
|
||||||
|
if target.Region != "" {
|
||||||
|
targetCfg.Region = target.Region
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := getOpts(opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("EC2 client options are invalid: %w", err)
|
||||||
|
}
|
||||||
|
// Dual-stack is the worker's path to AWS endpoints, not per-account. Apply
|
||||||
|
// it on the hopped client the same way EC2Client does for the principal.
|
||||||
|
var ec2Opts []func(*ec2.Options)
|
||||||
|
if parsed.withDualStack {
|
||||||
|
ec2Opts = append(ec2Opts, ec2.WithEndpointResolverV2(&endpointResolver{
|
||||||
|
dualStack: parsed.withDualStack,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if principalCatalogState.testEC2APIFunc != nil {
|
||||||
|
return principalCatalogState.testEC2APIFunc(targetCfg)
|
||||||
|
}
|
||||||
|
return ec2.NewFromConfig(targetCfg, ec2Opts...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendDistinct will append the elements to the slice
|
// appendDistinct will append the elements to the slice
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||||
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||||
|
stsTypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
|
||||||
"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"
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||||
@@ -28,6 +30,38 @@ import (
|
|||||||
"google.golang.org/protobuf/testing/protocmp"
|
"google.golang.org/protobuf/testing/protocmp"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
)
|
)
|
||||||
|
// testRoleARN is a syntactically valid ARN used for role_arn test cases.
|
||||||
|
// All STS calls against it are handled by injected mocks — no real AWS
|
||||||
|
// account is contacted.
|
||||||
|
const testRoleARN = "arn:aws:iam::123456789012:role/BoundaryTestRole"
|
||||||
|
|
||||||
|
// validAssumeRoleOutput returns a mock AssumeRoleOutput with synthetic
|
||||||
|
// temporary credentials for use with WithSTSAPIFunc.
|
||||||
|
func validAssumeRoleOutput() *sts.AssumeRoleOutput {
|
||||||
|
return &sts.AssumeRoleOutput{
|
||||||
|
Credentials: &stsTypes.Credentials{
|
||||||
|
AccessKeyId: aws.String("ASIAmockassumedkey"),
|
||||||
|
SecretAccessKey: aws.String("mockassumedsecret"),
|
||||||
|
SessionToken: aws.String("mockassumedsessiontoken"),
|
||||||
|
Expiration: aws.Time(time.Now().Add(time.Hour)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// catalogAttrsStruct builds a catalog attributes structpb for the given
|
||||||
|
// region. roleArn is written only when non-empty. disableRotation must be
|
||||||
|
// set explicitly to avoid implicit rotation behaviour in tests.
|
||||||
|
func catalogAttrsStruct(region, roleArn string, disableRotation bool) *structpb.Struct {
|
||||||
|
fields := map[string]*structpb.Value{
|
||||||
|
credential.ConstRegion: structpb.NewStringValue(region),
|
||||||
|
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(disableRotation),
|
||||||
|
}
|
||||||
|
if roleArn != "" {
|
||||||
|
fields[credential.ConstRoleArn] = structpb.NewStringValue(roleArn)
|
||||||
|
}
|
||||||
|
return &structpb.Struct{Fields: fields}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func TestPluginOnCreateCatalogSuccess(t *testing.T) {
|
func TestPluginOnCreateCatalogSuccess(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -138,17 +172,13 @@ func TestPluginOnCreateCatalogSuccess(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// role_arn set, no static keys. AssumeRole mock succeeds.
|
||||||
|
// Persisted secrets are empty — dynamic credentials are not stored.
|
||||||
name: "usingAssumeRole",
|
name: "usingAssumeRole",
|
||||||
req: &pb.OnCreateCatalogRequest{
|
req: &pb.OnCreateCatalogRequest{
|
||||||
Catalog: &hostcatalogs.HostCatalog{
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
Attributes: &structpb.Struct{
|
Attributes: catalogAttrsStruct("us-east-1", testRoleARN, true),
|
||||||
Fields: map[string]*structpb.Value{
|
|
||||||
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
|
|
||||||
credential.ConstRoleArn: structpb.NewStringValue("arn:0123:test:rolearn"),
|
|
||||||
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -158,6 +188,13 @@ func TestPluginOnCreateCatalogSuccess(t *testing.T) {
|
|||||||
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
|
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
|
credOpts: []credential.AwsCredentialPersistedStateOption{
|
||||||
|
credential.WithStateTestOpts([]awsutil.Option{
|
||||||
|
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(
|
||||||
|
awsutil.WithAssumeRoleOutput(validAssumeRoleOutput()),
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
},
|
||||||
expRsp: &pb.OnCreateCatalogResponse{Persisted: &pb.HostCatalogPersisted{Secrets: &structpb.Struct{}}},
|
expRsp: &pb.OnCreateCatalogResponse{Persisted: &pb.HostCatalogPersisted{Secrets: &structpb.Struct{}}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -315,6 +352,20 @@ func TestPluginOnUpdateCatalogSuccess(t *testing.T) {
|
|||||||
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
|
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
|
credOpts: []credential.AwsCredentialPersistedStateOption{
|
||||||
|
credential.WithStateTestOpts([]awsutil.Option{
|
||||||
|
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(
|
||||||
|
awsutil.WithAssumeRoleOutput(&sts.AssumeRoleOutput{
|
||||||
|
Credentials: &stsTypes.Credentials{
|
||||||
|
AccessKeyId: aws.String("ASIAfoobar"),
|
||||||
|
SecretAccessKey: aws.String("secretkeyfoobar"),
|
||||||
|
SessionToken: aws.String("sessiontokenfoobar"),
|
||||||
|
Expiration: aws.Time(time.Now().Add(time.Hour)),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
},
|
||||||
expRsp: &pb.OnUpdateCatalogResponse{
|
expRsp: &pb.OnUpdateCatalogResponse{
|
||||||
Persisted: &pb.HostCatalogPersisted{Secrets: &structpb.Struct{}},
|
Persisted: &pb.HostCatalogPersisted{Secrets: &structpb.Struct{}},
|
||||||
},
|
},
|
||||||
@@ -421,6 +472,16 @@ func TestPluginOnUpdateCatalogSuccess(t *testing.T) {
|
|||||||
credOpts: []credential.AwsCredentialPersistedStateOption{
|
credOpts: []credential.AwsCredentialPersistedStateOption{
|
||||||
credential.WithStateTestOpts([]awsutil.Option{
|
credential.WithStateTestOpts([]awsutil.Option{
|
||||||
awsutil.WithIAMAPIFunc(awsutil.NewMockIAM()),
|
awsutil.WithIAMAPIFunc(awsutil.NewMockIAM()),
|
||||||
|
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(
|
||||||
|
awsutil.WithAssumeRoleOutput(&sts.AssumeRoleOutput{
|
||||||
|
Credentials: &stsTypes.Credentials{
|
||||||
|
AccessKeyId: aws.String("ASIAfoobar"),
|
||||||
|
SecretAccessKey: aws.String("secretkeyfoobar"),
|
||||||
|
SessionToken: aws.String("sessiontokenfoobar"),
|
||||||
|
Expiration: aws.Time(time.Now().Add(time.Hour)),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
expRsp: &pb.OnUpdateCatalogResponse{
|
expRsp: &pb.OnUpdateCatalogResponse{
|
||||||
@@ -742,8 +803,40 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
|
|||||||
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
expectedErrContains: "aws describe instances failed: DescribeInstances error",
|
// checkHosts returns the DescribeInstances error as-is rather than
|
||||||
expectedErrCode: codes.FailedPrecondition,
|
// wrapping it in a gRPC FailedPrecondition status like the old
|
||||||
|
// dryRunValidation did. A plain Go error maps to codes.Unknown.
|
||||||
|
expectedErrContains: testDescribeInstancesError,
|
||||||
|
expectedErrCode: codes.Unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// BUG FIX: role_arn set, no static keys. AssumeRole mock returns
|
||||||
|
// an error. ValidateCreds now eagerly calls Retrieve() which forces
|
||||||
|
// sts:AssumeRole, so the error surfaces here at creation time.
|
||||||
|
// Previously this succeeded silently via the ambient credential chain.
|
||||||
|
name: "role_arn AssumeRole error — correctly rejected",
|
||||||
|
req: &pb.OnCreateCatalogRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: catalogAttrsStruct("us-east-1", testRoleARN, true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
credOpts: []credential.AwsCredentialPersistedStateOption{
|
||||||
|
credential.WithStateTestOpts([]awsutil.Option{
|
||||||
|
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(
|
||||||
|
awsutil.WithAssumeRoleError(errors.New("simulated AssumeRole failure")),
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
catalogOpts: []awsCatalogPersistedStateOption{
|
||||||
|
withTestEC2APIFunc(newTestMockEC2(
|
||||||
|
nil,
|
||||||
|
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
expectedErrContains: "simulated AssumeRole failure",
|
||||||
|
expectedErrCode: codes.Unknown,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -994,8 +1087,11 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
|||||||
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
expectedErrContains: "aws describe instances failed: DescribeInstances error",
|
//checkHosts returns the DesribeInstances errror as-is rather than wrapping it
|
||||||
expectedErrCode: codes.FailedPrecondition,
|
// in a gRpc FailedPrecodndition status like the old dryRunValidiation did.
|
||||||
|
// A plain Go error maps to codeds.Unknown.
|
||||||
|
expectedErrContains: testDescribeInstancesError,
|
||||||
|
expectedErrCode: codes.Unknown,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "new incoming dynamic credential update dry run error",
|
name: "new incoming dynamic credential update dry run error",
|
||||||
@@ -1030,14 +1126,27 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// ValidateCreds is called for DynamicAWS credentials before the dry-run
|
||||||
|
// DescribeInstances. Provide a mock STS that returns a successful AssumeRole
|
||||||
|
// so the validate step passes and execution reaches the EC2 mock.
|
||||||
|
credOpts: []credential.AwsCredentialPersistedStateOption{
|
||||||
|
credential.WithStateTestOpts([]awsutil.Option{
|
||||||
|
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(
|
||||||
|
awsutil.WithAssumeRoleOutput(validAssumeRoleOutput()),
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
},
|
||||||
catalogOpts: []awsCatalogPersistedStateOption{
|
catalogOpts: []awsCatalogPersistedStateOption{
|
||||||
withTestEC2APIFunc(newTestMockEC2(
|
withTestEC2APIFunc(newTestMockEC2(
|
||||||
nil,
|
nil,
|
||||||
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
expectedErrContains: "aws describe instances failed: DescribeInstances error",
|
// checkHosts returns the DescribeInstances error as-is rather than wrapping it
|
||||||
expectedErrCode: codes.FailedPrecondition,
|
// in a gRPC FailedPrecondition status like the old dryRunValidiation did.
|
||||||
|
// A plain Go error maps to codes.Unknown.
|
||||||
|
expectedErrContains: testDescribeInstancesError,
|
||||||
|
expectedErrCode: codes.Unknown,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "replace creds error",
|
name: "replace creds error",
|
||||||
@@ -1211,8 +1320,11 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
|
|||||||
testMockEC2WithDescribeInstancesError(fmt.Errorf("oops there was an error")),
|
testMockEC2WithDescribeInstancesError(fmt.Errorf("oops there was an error")),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
expectedErrContains: "aws describe instances failed: oops there was an error",
|
// checkHosts returns the DescribeInstances error as-is rather than wrapping it
|
||||||
expectedErrCode: codes.FailedPrecondition,
|
// in a gRPC FailedPrecondition status like the old dryRunValidiation did.
|
||||||
|
// A plain Go error maps to codes.Unknown.
|
||||||
|
expectedErrContains: "oops there was an error",
|
||||||
|
expectedErrCode: codes.Unknown,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1565,8 +1677,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
|
|||||||
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
expectedErrContains: fmt.Sprintf("aws describe instances failed: %s", testDescribeInstancesError),
|
expectedErrContains: testDescribeInstancesError,
|
||||||
expectedErrCode: codes.FailedPrecondition,
|
expectedErrCode: codes.Unknown,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1828,8 +1940,8 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
|||||||
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
expectedErrContains: fmt.Sprintf("aws describe instances failed: %s", testDescribeInstancesError),
|
expectedErrContains: testDescribeInstancesError,
|
||||||
expectedErrCode: codes.FailedPrecondition,
|
expectedErrCode: codes.Unknown,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1849,6 +1961,162 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testListHostsSet(id, filter string) *hostsets.HostSet {
|
||||||
|
return &hostsets.HostSet{
|
||||||
|
Id: id,
|
||||||
|
Attrs: &hostsets.HostSet_Attributes{
|
||||||
|
Attributes: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
ConstDescribeInstancesFilters: structpb.NewListValue(
|
||||||
|
&structpb.ListValue{
|
||||||
|
Values: []*structpb.Value{
|
||||||
|
structpb.NewStringValue(filter),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testListHostsPersisted() *pb.HostCatalogPersisted {
|
||||||
|
return &pb.HostCatalogPersisted{
|
||||||
|
Secrets: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||||
|
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExpectedDescribeInput(filterKey, filterValue string) *ec2.DescribeInstancesInput {
|
||||||
|
return &ec2.DescribeInstancesInput{
|
||||||
|
DryRun: aws.Bool(false),
|
||||||
|
Filters: []types.Filter{
|
||||||
|
{
|
||||||
|
Name: aws.String(filterKey),
|
||||||
|
Values: []string{filterValue},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: aws.String("instance-state-name"),
|
||||||
|
Values: []string{string(types.InstanceStateNameRunning)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginListHosts(t *testing.T) {
|
||||||
|
describeOutput := &ec2.DescribeInstancesOutput{
|
||||||
|
Reservations: []types.Reservation{
|
||||||
|
{
|
||||||
|
Instances: []types.Instance{
|
||||||
|
{
|
||||||
|
InstanceId: aws.String("i-shared"),
|
||||||
|
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
catalogFields map[string]*structpb.Value
|
||||||
|
sets []*hostsets.HostSet
|
||||||
|
expectedRegion string
|
||||||
|
expectedHosts []*pb.ListHostsResponseHost
|
||||||
|
expectedInputs []*ec2.DescribeInstancesInput
|
||||||
|
expectedDescribe int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "principal account lists hosts",
|
||||||
|
catalogFields: map[string]*structpb.Value{
|
||||||
|
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||||
|
},
|
||||||
|
sets: []*hostsets.HostSet{
|
||||||
|
testListHostsSet("set-1", "tag-key=foo"),
|
||||||
|
},
|
||||||
|
expectedRegion: "us-west-2",
|
||||||
|
expectedHosts: []*pb.ListHostsResponseHost{
|
||||||
|
{
|
||||||
|
ExternalId: "i-shared",
|
||||||
|
SetIds: []string{"set-1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedInputs: []*ec2.DescribeInstancesInput{
|
||||||
|
testExpectedDescribeInput("tag-key", "foo"),
|
||||||
|
},
|
||||||
|
expectedDescribe: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target account lists hosts without querying principal",
|
||||||
|
catalogFields: map[string]*structpb.Value{
|
||||||
|
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||||
|
credential.ConstTargetRoleArn: structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
|
||||||
|
credential.ConstTargetRegion: structpb.NewStringValue("eu-west-1"),
|
||||||
|
},
|
||||||
|
sets: []*hostsets.HostSet{
|
||||||
|
testListHostsSet("set-1", "tag-key=foo"),
|
||||||
|
testListHostsSet("set-2", "tag-key=bar"),
|
||||||
|
},
|
||||||
|
expectedRegion: "eu-west-1",
|
||||||
|
expectedHosts: []*pb.ListHostsResponseHost{
|
||||||
|
{
|
||||||
|
ExternalId: "i-shared",
|
||||||
|
SetIds: []string{"set-1", "set-2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedInputs: []*ec2.DescribeInstancesInput{
|
||||||
|
testExpectedDescribeInput("tag-key", "foo"),
|
||||||
|
testExpectedDescribeInput("tag-key", "bar"),
|
||||||
|
},
|
||||||
|
expectedDescribe: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
state := &testMockEC2State{}
|
||||||
|
p := &HostPlugin{
|
||||||
|
testCatalogStateOpts: []awsCatalogPersistedStateOption{
|
||||||
|
withTestEC2APIFunc(newTestMockEC2(
|
||||||
|
state,
|
||||||
|
testMockEC2WithDescribeInstancesOutput(describeOutput),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := p.ListHosts(context.Background(), &pb.ListHostsRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: &structpb.Struct{Fields: tc.catalogFields},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Persisted: testListHostsPersisted(),
|
||||||
|
Sets: tc.sets,
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(resp)
|
||||||
|
require.Len(resp.GetHosts(), len(tc.expectedHosts))
|
||||||
|
for i, expected := range tc.expectedHosts {
|
||||||
|
require.Equal(expected.ExternalId, resp.Hosts[i].ExternalId)
|
||||||
|
require.Equal(expected.SetIds, resp.Hosts[i].SetIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(tc.expectedDescribe, state.DescribeInstancesCallCount)
|
||||||
|
require.Len(state.ClientRegions, tc.expectedDescribe)
|
||||||
|
for _, region := range state.ClientRegions {
|
||||||
|
require.Equal(tc.expectedRegion, region)
|
||||||
|
}
|
||||||
|
require.Equal(tc.expectedInputs, state.DescribeInstancesInputs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPluginListHostsErr(t *testing.T) {
|
func TestPluginListHostsErr(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -2130,7 +2398,101 @@ func TestPluginListHostsErr(t *testing.T) {
|
|||||||
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
expectedErrContains: fmt.Sprintf("error running DescribeInstances for host set id \"foobar\": %s", testDescribeInstancesError),
|
expectedErrContains: fmt.Sprintf("error retrieving host results for host set id \"foobar\": %s", testDescribeInstancesError),
|
||||||
|
expectedErrCode: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target attributes missing target_role_arn",
|
||||||
|
req: &pb.ListHostsRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||||
|
credential.ConstTargetRegion: structpb.NewStringValue("eu-west-1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"",
|
||||||
|
expectedErrCode: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target EC2 client error",
|
||||||
|
req: &pb.ListHostsRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||||
|
credential.ConstTargetRoleArn: structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
|
Secrets: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
|
||||||
|
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
|
||||||
|
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sets: []*hostsets.HostSet{
|
||||||
|
{
|
||||||
|
Id: "foobar",
|
||||||
|
Attrs: &hostsets.HostSet_Attributes{
|
||||||
|
Attributes: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
ConstDescribeInstancesFilters: structpb.NewListValue(
|
||||||
|
&structpb.ListValue{
|
||||||
|
Values: []*structpb.Value{
|
||||||
|
structpb.NewStringValue("tag-key=foo"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
catalogOpts: []awsCatalogPersistedStateOption{
|
||||||
|
withTestEC2APIFunc(func(...aws.Config) (EC2API, error) {
|
||||||
|
return nil, errors.New("test EC2 client error")
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedErrContains: "unable to list hosts for host set \"foobar\": EC2 client setup failed for target account: test EC2 client error",
|
||||||
|
expectedErrCode: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target DescribeInstances error",
|
||||||
|
req: &pb.ListHostsRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
|
||||||
|
credential.ConstTargetRoleArn: structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
|
||||||
|
credential.ConstTargetRegion: structpb.NewStringValue("eu-west-1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Persisted: testListHostsPersisted(),
|
||||||
|
Sets: []*hostsets.HostSet{
|
||||||
|
testListHostsSet("foobar", "tag-key=foo"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
catalogOpts: []awsCatalogPersistedStateOption{
|
||||||
|
withTestEC2APIFunc(newTestMockEC2(
|
||||||
|
nil,
|
||||||
|
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
expectedErrContains: "unable to list hosts for host set \"foobar\": EC2 DescribeInstances failed for target account: DescribeInstances error",
|
||||||
expectedErrCode: codes.InvalidArgument,
|
expectedErrCode: codes.InvalidArgument,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2383,6 +2745,21 @@ func TestBuildDescribeInstancesInput(t *testing.T) {
|
|||||||
require.Equal(tc.expected, actual)
|
require.Equal(tc.expected, actual)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Run("nil attrs, dry run", func(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
actual, err := buildDescribeInstancesInput(nil, true)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(&ec2.DescribeInstancesInput{
|
||||||
|
DryRun: aws.Bool(true),
|
||||||
|
Filters: []types.Filter{
|
||||||
|
{
|
||||||
|
Name: aws.String("instance-state-name"),
|
||||||
|
Values: []string{string(types.InstanceStateNameRunning)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, actual)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAwsInstanceToHost(t *testing.T) {
|
func TestAwsInstanceToHost(t *testing.T) {
|
||||||
@@ -2821,54 +3198,3 @@ func TestAppendDistinct(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDryRunValidation(t *testing.T) {
|
|
||||||
t.Run("nil credential state", func(t *testing.T) {
|
|
||||||
st := dryRunValidation(context.Background(), nil, nil)
|
|
||||||
require.NotNil(t, st)
|
|
||||||
require.Equal(t, codes.InvalidArgument.String(), st.Code().String())
|
|
||||||
require.Equal(t, "persisted state is required", st.Message())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ec2ClientErr", func(t *testing.T) {
|
|
||||||
st := dryRunValidation(context.Background(), &awsCatalogPersistedState{
|
|
||||||
AwsCredentialPersistedState: &credential.AwsCredentialPersistedState{
|
|
||||||
CredentialsConfig: &awsutil.CredentialsConfig{},
|
|
||||||
},
|
|
||||||
testEC2APIFunc: func(...aws.Config) (EC2API, error) {
|
|
||||||
return nil, fmt.Errorf("oops ec2 client err")
|
|
||||||
},
|
|
||||||
}, []ec2Option{})
|
|
||||||
require.NotNil(t, st)
|
|
||||||
require.Equal(t, codes.InvalidArgument.String(), st.Code().String())
|
|
||||||
require.Equal(t, "error getting EC2 client: oops ec2 client err", st.Message())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("describeInstancesErr", func(t *testing.T) {
|
|
||||||
st := dryRunValidation(context.Background(), &awsCatalogPersistedState{
|
|
||||||
AwsCredentialPersistedState: &credential.AwsCredentialPersistedState{
|
|
||||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
|
||||||
AccessKey: "AKIAfoo",
|
|
||||||
SecretKey: "baz",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
testEC2APIFunc: newTestMockEC2(nil, testMockEC2WithDescribeInstancesError(fmt.Errorf("oops describe instances error"))),
|
|
||||||
}, []ec2Option{})
|
|
||||||
require.NotNil(t, st)
|
|
||||||
require.Equal(t, codes.FailedPrecondition.String(), st.Code().String())
|
|
||||||
require.Equal(t, "aws describe instances failed: oops describe instances error", st.Message())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("success", func(t *testing.T) {
|
|
||||||
st := dryRunValidation(context.Background(), &awsCatalogPersistedState{
|
|
||||||
AwsCredentialPersistedState: &credential.AwsCredentialPersistedState{
|
|
||||||
CredentialsConfig: &awsutil.CredentialsConfig{
|
|
||||||
AccessKey: "AKIAfoo",
|
|
||||||
SecretKey: "baz",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
testEC2APIFunc: newTestMockEC2(nil, testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{})),
|
|
||||||
}, []ec2Option{})
|
|
||||||
require.Nil(t, st)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -22,11 +22,17 @@ const (
|
|||||||
type testMockEC2State struct {
|
type testMockEC2State struct {
|
||||||
DescribeInstancesCalled bool
|
DescribeInstancesCalled bool
|
||||||
DescribeInstancesInputParams *ec2.DescribeInstancesInput
|
DescribeInstancesInputParams *ec2.DescribeInstancesInput
|
||||||
|
DescribeInstancesCallCount int
|
||||||
|
DescribeInstancesInputs []*ec2.DescribeInstancesInput
|
||||||
|
ClientRegions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *testMockEC2State) Reset() {
|
func (s *testMockEC2State) Reset() {
|
||||||
s.DescribeInstancesCalled = false
|
s.DescribeInstancesCalled = false
|
||||||
s.DescribeInstancesInputParams = nil
|
s.DescribeInstancesInputParams = nil
|
||||||
|
s.DescribeInstancesCallCount = 0
|
||||||
|
s.DescribeInstancesInputs = nil
|
||||||
|
s.ClientRegions = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type testMockEC2 struct {
|
type testMockEC2 struct {
|
||||||
@@ -72,7 +78,10 @@ func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFu
|
|||||||
if cfg.Region != "" {
|
if cfg.Region != "" {
|
||||||
m.Region = cfg.Region
|
m.Region = cfg.Region
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if state != nil {
|
||||||
|
state.ClientRegions = append(state.ClientRegions, m.Region)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -83,6 +92,8 @@ func (m *testMockEC2) DescribeInstances(ctx context.Context, input *ec2.Describe
|
|||||||
if m.State != nil {
|
if m.State != nil {
|
||||||
m.State.DescribeInstancesCalled = true
|
m.State.DescribeInstancesCalled = true
|
||||||
m.State.DescribeInstancesInputParams = input
|
m.State.DescribeInstancesInputParams = input
|
||||||
|
m.State.DescribeInstancesCallCount++
|
||||||
|
m.State.DescribeInstancesInputs = append(m.State.DescribeInstancesInputs, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.DescribeInstancesError != nil {
|
if m.DescribeInstancesError != nil {
|
||||||
|
|||||||
Reference in new issue
Block a user