Compare commits

...
1 Commits
Author SHA1 Message Date
Louis Ruch 716052077b s 2026-05-13 20:38:49 -07:00
6 changed files with 931 additions and 24 deletions

No files matched your search

+40 -1
View File
@@ -16,6 +16,36 @@ boundary host-catalogs create plugin \
-secret secret_access_key='SECRET'
```
To create a host catalog that returns addresses only from the primary network
interface:
```
boundary host-catalogs create plugin \
-scope-id p_1234567890 \
-name "Example Plugin-Based Host Catalog" \
-description "Description for plugin-based host catalog" \
-plugin-name aws \
-attr region=REGION \
-attr primary_interface_only=true \
-secret access_key_id='KEY' \
-secret secret_access_key='SECRET'
```
To create a host catalog that excludes public IPv4 addresses while keeping
private IPv4 addresses:
```
boundary host-catalogs create plugin \
-scope-id p_1234567890 \
-name "Example Plugin-Based Host Catalog" \
-description "Description for plugin-based host catalog" \
-plugin-name aws \
-attr region=REGION \
-attr exclude_public_ips=true \
-secret access_key_id='KEY' \
-secret secret_access_key='SECRET'
```
To create a host set, filtering the host set based on tag keys `foo` or `bar`
(either tag can be present), ensuring that any targets set to this host set only
connect to external addresses in the `54.0.0.0/8` class A subnet:
@@ -75,7 +105,6 @@ a configured IAM user for this provider:
"Resource": "*"
}
]
}
```
* `iam:GetUser`, `iam:CreateAccessKey`, and `iam:DeleteAccessKey`, configured to
@@ -116,6 +145,16 @@ The following `attributes` are valid on an AWS host catalog resource:
provider.
- `role_tags` (object): The key-value pair tags configured for the `AssumeRole`
provider.
- `primary_interface_only` (bool): If `true`, only addresses from the primary
ENI are returned. When `false` or omitted, addresses from all attached ENIs
are considered.
- `exclude_private_ips` (bool): If `true`, private IPv4 addresses are omitted.
- `exclude_public_ips` (bool): If `true`, public IPv4 addresses are omitted.
If none of the exclusion flags are set, the plugin preserves the historical
behavior and returns all discovered address classes. The exclusion flags apply
to both IPv4 and IPv6 addresses based on whether the IP is classified as public
or private.
The following `secrets` are required on an AWS host catalog resource:
+46
View File
@@ -22,6 +22,18 @@ type CatalogAttributes struct {
// DualStack is used for configuring how the aws client will resolve requests.
DualStack bool
// PrimaryInterfaceOnly restricts discovery to the instance's primary ENI.
PrimaryInterfaceOnly bool
// ExcludePrivateIps controls whether private IPv4 addresses are omitted.
ExcludePrivateIps bool
// ExcludePublicIps controls whether public IPv4 addresses are omitted.
ExcludePublicIps bool
// ExcludeIpv6 controls whether IPv6 addresses are omitted.
ExcludeIpv6 bool
}
func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
@@ -40,6 +52,36 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
}
delete(unknownFields, ConstAwsDualStack)
primaryInterfaceOnly, err := values.GetBoolValue(in, ConstPrimaryInterfaceOnly, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstPrimaryInterfaceOnly)] = err.Error()
}
delete(unknownFields, ConstPrimaryInterfaceOnly)
excludePrivateIPs, err := values.GetBoolValue(in, ConstExcludePrivateIps, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstExcludePrivateIps)] = err.Error()
}
delete(unknownFields, ConstExcludePrivateIps)
excludePublicIPs, err := values.GetBoolValue(in, ConstExcludePublicIps, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstExcludePublicIps)] = err.Error()
}
delete(unknownFields, ConstExcludePublicIps)
excludeIpv6, err := values.GetBoolValue(in, ConstExcludeIpv6, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstExcludeIpv6)] = err.Error()
}
delete(unknownFields, ConstExcludeIpv6)
if excludePrivateIPs && excludePublicIPs && excludeIpv6 {
badFields[fmt.Sprintf("attributes.%s", ConstExcludePrivateIps)] = "cannot be combined with exclude_public_ips and exclude_ipv6"
badFields[fmt.Sprintf("attributes.%s", ConstExcludePublicIps)] = "cannot be combined with exclude_private_ips and exclude_ipv6"
badFields[fmt.Sprintf("attributes.%s", ConstExcludeIpv6)] = "cannot be combined with exclude_private_ips and exclude_public_ips"
}
for s := range unknownFields {
switch s {
// Ignore knownFields from CredentialAttributes
@@ -67,6 +109,10 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
return &CatalogAttributes{
CredentialAttributes: credAttributes,
DualStack: dualStack,
PrimaryInterfaceOnly: primaryInterfaceOnly,
ExcludePrivateIps: excludePrivateIPs,
ExcludePublicIps: excludePublicIPs,
ExcludeIpv6: excludeIpv6,
}, nil
}
+100
View File
@@ -59,6 +59,106 @@ func TestGetCatalogAttributes(t *testing.T) {
DualStack: true,
},
},
{
name: "with primary interface only",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"primary_interface_only": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
PrimaryInterfaceOnly: true,
},
},
{
name: "with exclude public ip",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_public_ips": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePublicIps: true,
},
},
{
name: "with exclude private ip",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePrivateIps: true,
},
},
{
name: "with mixed attributes",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"dual_stack": structpb.NewBoolValue(true),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
"primary_interface_only": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
DualStack: true,
ExcludePrivateIps: true,
ExcludeIpv6: true,
PrimaryInterfaceOnly: true,
},
},
{
name: "can exclude public and private IPs when IPv6 remains enabled",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_public_ips": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePrivateIps: true,
ExcludePublicIps: true,
},
},
{
name: "cannot exclude public private and IPv6 together",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_public_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expectedErrContains: "attributes.exclude_ipv6: cannot be combined with exclude_private_ips and exclude_public_ips, attributes.exclude_private_ips: cannot be combined with exclude_public_ips and exclude_ipv6, attributes.exclude_public_ips: cannot be combined with exclude_private_ips and exclude_ipv6",
},
{
name: "default",
in: &structpb.Struct{
+12
View File
@@ -13,4 +13,16 @@ const (
// ConstAwsDualStack is the key for the dualstack flag in the aws s3 client.
ConstAwsDualStack = "dual_stack"
// ConstPrimaryInterfaceOnly limits host discovery to the primary ENI.
ConstPrimaryInterfaceOnly = "primary_interface_only"
// ConstExcludePrivateIps excludes private IPv4 addresses.
ConstExcludePrivateIps = "exclude_private_ips"
// ConstExcludePublicIps excludes public IPv4 addresses associated to ENI IPs.
ConstExcludePublicIps = "exclude_public_ips"
// ConstExcludeIpv6 excludes IPv6 addresses.
ConstExcludeIpv6 = "exclude_ipv6"
)
+41 -12
View File
@@ -1,8 +1,8 @@
package host
// Copyright IBM Corp. 2021, 2026
// SPDX-License-Identifier: MPL-2.0
package host
import (
"context"
"fmt"
@@ -550,7 +550,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
// set of hosts afterwards (possibly removing duplicates).
for _, reservation := range output.Reservations {
for _, instance := range reservation.Instances {
host, err := awsInstanceToHost(instance)
host, err := awsInstanceToHost(instance, catalogAttributes)
if err != nil {
return nil, errors.BadRequestStatus("error processing host results for host set id %q: %s", query.Id, err)
}
@@ -648,10 +648,13 @@ func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.Descri
// awsInstanceToHost processes data from an ec2.Instance and returns
// a ListHostsResponseHost with the ID and network addresses
// populated.
func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, error) {
func awsInstanceToHost(instance types.Instance, catalogAttributes *CatalogAttributes) (*pb.ListHostsResponseHost, error) {
if aws.ToString(instance.InstanceId) == "" {
return nil, fmt.Errorf("response integrity error: missing instance id")
}
if catalogAttributes == nil {
return nil, fmt.Errorf("catalog attributes are required")
}
result := new(pb.ListHostsResponseHost)
@@ -666,22 +669,46 @@ func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, erro
}
}
result.IpAddresses = appendDistinct(result.IpAddresses, instance.PrivateIpAddress, instance.PublicIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, instance.PrivateDnsName, instance.PublicDnsName)
if !catalogAttributes.ExcludePrivateIps {
result.IpAddresses = appendDistinct(result.IpAddresses, instance.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, instance.PrivateDnsName)
}
if !catalogAttributes.ExcludePublicIps {
result.IpAddresses = appendDistinct(result.IpAddresses, instance.PublicIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, instance.PublicDnsName)
}
// Now go through all of the interfaces and log the IP address of
// every interface.
for _, iface := range instance.NetworkInterfaces {
if catalogAttributes.PrimaryInterfaceOnly {
if iface.Attachment == nil || aws.ToInt32(iface.Attachment.DeviceIndex) != 0 {
// We only want to sync the primary interface so skip any others
continue
}
}
// Populate default IP addresses/DNS name similar to how we do
// for the entire instance.
result.IpAddresses = appendDistinct(result.IpAddresses, iface.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, iface.PrivateDnsName)
if !catalogAttributes.ExcludePrivateIps {
result.IpAddresses = appendDistinct(result.IpAddresses, iface.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, iface.PrivateDnsName)
}
// Iterate through the private IP addresses and log the
// information.
for _, addr := range iface.PrivateIpAddresses {
result.IpAddresses = appendDistinct(result.IpAddresses, addr.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, addr.PrivateDnsName)
if !catalogAttributes.ExcludePrivateIps {
result.IpAddresses = appendDistinct(result.IpAddresses, addr.PrivateIpAddress)
result.DnsNames = appendDistinct(result.DnsNames, addr.PrivateDnsName)
}
if catalogAttributes.ExcludePublicIps {
// skip adding public addresses if we are excluding them
continue
}
// PublicIP for iface is only available via an association to the private IP
if addr.Association != nil {
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Association.PublicIp)
result.DnsNames = appendDistinct(result.DnsNames, addr.Association.PublicDnsName)
@@ -689,8 +716,10 @@ func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, erro
}
// Add the IPv6 addresses.
for _, addr := range iface.Ipv6Addresses {
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Ipv6Address)
if !catalogAttributes.ExcludeIpv6 {
for _, addr := range iface.Ipv6Addresses {
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Ipv6Address)
}
}
}
+692 -11
View File
@@ -638,7 +638,8 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
},
},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
@@ -664,7 +665,8 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
},
},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
@@ -694,7 +696,8 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
},
},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
@@ -721,7 +724,8 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
},
},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
@@ -2383,10 +2387,11 @@ func TestBuildDescribeInstancesInput(t *testing.T) {
func TestAwsInstanceToHost(t *testing.T) {
cases := []struct {
name string
instance types.Instance
expected *pb.ListHostsResponseHost
expectedErr string
name string
instance types.Instance
catalogAttributes *CatalogAttributes
expected *pb.ListHostsResponseHost
expectedErr string
}{
{
name: "missing instance id",
@@ -2591,14 +2596,45 @@ func TestAwsInstanceToHost(t *testing.T) {
},
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: nil}, // Just coverage for nil assertion which is skipped
{Ipv6Address: aws.String("some::fake::address")},
{Ipv6Address: aws.String("2001:db8::1")},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "some::fake::address"},
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "2001:db8::1"},
DnsNames: []string{"test.example.internal", "test.example.com"},
},
},
{
name: "good, single IP w/public addr",
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1"},
DnsNames: []string{"test.example.internal", "test.example.com"},
},
},
@@ -2676,18 +2712,663 @@ func TestAwsInstanceToHost(t *testing.T) {
DnsNames: []string{"test.example.internal", "test.example.com"},
},
},
{
name: "primary_interface_only keeps all addresses on primary ENI",
catalogAttributes: &CatalogAttributes{
PrimaryInterfaceOnly: true,
},
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(0),
},
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Primary: aws.Bool(true),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
},
// Secondary IPs on primary ENI (e.g. VPC CNI pod IPs).
{
Primary: aws.Bool(false),
PrivateIpAddress: aws.String("10.0.0.42"),
PrivateDnsName: aws.String("pod-a.example.internal"),
},
{
Primary: aws.Bool(false),
PrivateIpAddress: aws.String("10.0.0.43"),
PrivateDnsName: aws.String("pod-b.example.internal"),
},
},
},
// Secondary ENI attached by VPC CNI for additional pod IPs.
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(1),
},
PrivateIpAddress: aws.String("10.0.0.99"),
PrivateDnsName: aws.String("eni1.example.internal"),
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "10.0.0.42", "10.0.0.43"},
DnsNames: []string{"test.example.internal", "test.example.com", "pod-a.example.internal", "pod-b.example.internal"},
},
},
{
name: "primary_interface_only keeps IPv6 on primary ENI",
catalogAttributes: &CatalogAttributes{
PrimaryInterfaceOnly: true,
},
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
NetworkInterfaces: []types.InstanceNetworkInterface{
// Secondary ENI listed first to verify ordering does not matter.
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(1),
},
PrivateIpAddress: aws.String("10.0.0.99"),
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: aws.String("2001:db8::2")},
},
},
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(0),
},
PrivateIpAddress: aws.String("10.0.0.1"),
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: aws.String("2001:db8::3")},
{Ipv6Address: aws.String("2001:db8::4")},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "2001:db8::3", "2001:db8::4"},
DnsNames: []string{"test.example.internal"},
},
},
{
name: "primary_interface_only without interfaces still returns top-level IPs",
catalogAttributes: &CatalogAttributes{
PrimaryInterfaceOnly: true,
},
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1"},
DnsNames: []string{"test.example.internal", "test.example.com"},
},
},
{
name: "exclude_public_ips excludes public IPv4 sources but keeps IPv6",
catalogAttributes: &CatalogAttributes{
ExcludePublicIps: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::1")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "2001:db8::1"},
DnsNames: []string{"host.internal"},
},
},
{
name: "exclude_private_ips excludes private IPv4 sources but keeps IPv6",
catalogAttributes: &CatalogAttributes{
ExcludePrivateIps: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{Association: &types.InstanceNetworkInterfaceAssociation{PublicIp: aws.String("1.2.3.4"), PublicDnsName: aws.String("host.public")}},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::1")}},
},
{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{Association: &types.InstanceNetworkInterfaceAssociation{PublicIp: aws.String("1.2.3.5"), PublicDnsName: aws.String("svc.public")}},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"1.2.3.4", "2001:db8::1", "1.2.3.5"},
DnsNames: []string{"host.public", "svc.public"},
},
},
{
name: "exclude_public_ips keeps IPv6 addresses",
catalogAttributes: &CatalogAttributes{
ExcludePublicIps: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("fd00::1")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "fd00::1"},
DnsNames: []string{"host.internal"},
},
},
{
name: "exclude_ipv6 omits IPv6 while preserving IPv4 and DNS",
catalogAttributes: &CatalogAttributes{
ExcludeIpv6: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::1")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "1.2.3.4"},
DnsNames: []string{"host.internal", "host.public"},
},
},
{
name: "exclude_public_ips with exclude_ipv6 keeps only private IPv4 sources",
catalogAttributes: &CatalogAttributes{
ExcludePublicIps: true,
ExcludeIpv6: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
{
PrivateIpAddress: aws.String("10.0.0.2"),
PrivateDnsName: aws.String("svc.internal"),
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::1")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "10.0.0.2"},
DnsNames: []string{"host.internal", "svc.internal"},
},
},
{
name: "exclude_private_ips with exclude_ipv6 keeps only public IPv4 sources",
catalogAttributes: &CatalogAttributes{
ExcludePrivateIps: true,
ExcludeIpv6: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.5"),
PublicDnsName: aws.String("svc.public"),
},
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("fd00::1")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"1.2.3.4", "1.2.3.5"},
DnsNames: []string{"host.public", "svc.public"},
},
},
{
name: "primary_interface_only with exclude_ipv6 drops IPv6 from primary ENI",
catalogAttributes: &CatalogAttributes{
PrimaryInterfaceOnly: true,
ExcludeIpv6: true,
},
instance: types.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PublicIpAddress: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(0),
},
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.42"),
PrivateDnsName: aws.String("pod-a.example.internal"),
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::3")}},
},
{
Attachment: &types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(1),
},
PrivateIpAddress: aws.String("10.0.0.99"),
PrivateDnsName: aws.String("eni1.example.internal"),
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::4")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "10.0.0.42"},
DnsNames: []string{"test.example.internal", "test.example.com", "pod-a.example.internal"},
},
},
{
name: "exclude_ipv6 on IPv6-only interface leaves no addresses",
catalogAttributes: &CatalogAttributes{
ExcludeIpv6: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: aws.String("2001:db8::1")},
{Ipv6Address: aws.String("fd00::1")},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
},
},
{
name: "deduplicates repeated addresses and DNS names across instance and interfaces",
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
},
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: aws.String("2001:db8::1")},
{Ipv6Address: aws.String("2001:db8::1")},
},
},
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
},
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: aws.String("2001:db8::1")},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "1.2.3.4", "2001:db8::1"},
DnsNames: []string{"host.internal", "host.public"},
},
},
{
name: "primary_interface_only skips interfaces without attachment metadata",
catalogAttributes: &CatalogAttributes{
PrimaryInterfaceOnly: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.99"),
PrivateDnsName: aws.String("missing-attachment.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.99"),
PrivateDnsName: aws.String("missing-attachment.internal"),
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("2001:db8::99")}},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"10.0.0.1", "1.2.3.4"},
DnsNames: []string{"host.internal", "host.public"},
},
},
{
name: "exclude_private_ips removes private DNS names even when public addresses remain",
catalogAttributes: &CatalogAttributes{
ExcludePrivateIps: true,
},
instance: types.Instance{
InstanceId: aws.String("i-1234"),
PublicIpAddress: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("host.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.4"),
PublicDnsName: aws.String("host.public"),
},
},
{
PrivateIpAddress: aws.String("10.0.0.2"),
PrivateDnsName: aws.String("svc.internal"),
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.2.3.5"),
PublicDnsName: aws.String("svc.public"),
},
},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "i-1234",
IpAddresses: []string{"1.2.3.4", "1.2.3.5"},
DnsNames: []string{"host.public", "svc.public"},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := awsInstanceToHost(tc.instance)
attrs := tc.catalogAttributes
if attrs == nil {
attrs = &CatalogAttributes{}
}
actual, err := awsInstanceToHost(tc.instance, attrs)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
func TestGetCatalogAttributesAddressSelection(t *testing.T) {
cases := []struct {
name string
in *structpb.Struct
expectedErr string
expected *CatalogAttributes
}{
{
name: "valid selectors",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"primary_interface_only": structpb.NewBoolValue(true),
"exclude_public_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
PrimaryInterfaceOnly: true,
ExcludePublicIps: true,
ExcludeIpv6: true,
},
},
{
name: "exclude_private_ips can be combined with exclude_ipv6",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePrivateIps: true,
ExcludeIpv6: true,
},
},
{
name: "exclude_public_ips can be combined with exclude_ipv6",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_public_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePublicIps: true,
ExcludeIpv6: true,
},
},
{
name: "exclude_private_ips and exclude_public_ips can be combined when IPv6 remains enabled",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_public_ips": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludePrivateIps: true,
ExcludePublicIps: true,
},
},
{
name: "exclude_private_ips and exclude_public_ips cannot also exclude_ipv6",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_private_ips": structpb.NewBoolValue(true),
"exclude_public_ips": structpb.NewBoolValue(true),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expectedErr: "attributes.exclude_ipv6: cannot be combined with exclude_private_ips and exclude_public_ips, attributes.exclude_private_ips: cannot be combined with exclude_public_ips and exclude_ipv6, attributes.exclude_public_ips: cannot be combined with exclude_private_ips and exclude_ipv6",
},
{
name: "exclude_ipv6 alone is accepted",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_ipv6": structpb.NewBoolValue(true),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
ExcludeIpv6: true,
},
},
{
name: "exclude_ipv6 requires a boolean",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
"exclude_ipv6": structpb.NewStringValue("true"),
},
},
expectedErr: "attributes.exclude_ipv6",
},
{
name: "no selectors configured preserves historical behavior",
in: &structpb.Struct{
Fields: map[string]*structpb.Value{
"region": structpb.NewStringValue("us-west-2"),
},
},
expected: &CatalogAttributes{
CredentialAttributes: &credential.CredentialAttributes{
Region: "us-west-2",
DisableCredentialRotation: false,
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := getCatalogAttributes(tc.in)
if tc.expectedErr != "" {
require.Error(err)
require.Contains(err.Error(), tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})