2023-03-20 11:02:17 -07:00
|
|
|
package host
|
|
|
|
|
|
2026-05-11 22:37:05 -07:00
|
|
|
// Copyright IBM Corp. 2021, 2026
|
2023-03-20 11:02:17 -07:00
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2023-07-04 11:03:24 -07:00
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
2024-09-12 16:46:55 +01:00
|
|
|
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
|
|
|
|
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
|
2023-03-20 11:02:17 -07:00
|
|
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
|
|
|
|
"google.golang.org/grpc/codes"
|
|
|
|
|
"google.golang.org/grpc/status"
|
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// HostPlugin implements the HostPluginServiceServer interface for the
|
|
|
|
|
// AWS host service plugin.
|
|
|
|
|
type HostPlugin struct {
|
|
|
|
|
pb.UnimplementedHostPluginServiceServer
|
|
|
|
|
|
|
|
|
|
// testCredStateOpts are passed in to the stored state to control test behavior
|
|
|
|
|
testCredStateOpts []credential.AwsCredentialPersistedStateOption
|
|
|
|
|
|
|
|
|
|
// testCatalogStateOpts are passed in to the stored state to control test behavior
|
|
|
|
|
testCatalogStateOpts []awsCatalogPersistedStateOption
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure that we are implementing HostPluginServiceServer
|
|
|
|
|
var _ pb.HostPluginServiceServer = (*HostPlugin)(nil)
|
|
|
|
|
|
|
|
|
|
// OnCreateCatalog is called when a dynamic host catalog is created.
|
|
|
|
|
func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
|
|
|
|
|
catalog := req.GetCatalog()
|
|
|
|
|
if catalog == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("catalog is required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attrs := catalog.GetAttributes()
|
|
|
|
|
if attrs == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("attributes are required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogAttributes, err := getCatalogAttributes(attrs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-10-28 11:50:18 -07:00
|
|
|
credConfig, err := credential.GetCredentialsConfig(catalog.GetSecrets(), catalogAttributes.CredentialAttributes, catalogAttributes.DualStack)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set up the creds in our persisted state.
|
|
|
|
|
credState, err := credential.NewAwsCredentialPersistedState(
|
|
|
|
|
append([]credential.AwsCredentialPersistedStateOption{
|
2023-07-18 16:33:24 -07:00
|
|
|
credential.WithCredentialsConfig(credConfig),
|
2023-03-20 11:02:17 -07:00
|
|
|
}, p.testCredStateOpts...)...,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
|
2024-09-12 16:46:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to rotate AWS static credentials
|
|
|
|
|
if credential.GetCredentialType(credState.CredentialsConfig) == credential.StaticAWS && !catalogAttributes.DisableCredentialRotation {
|
|
|
|
|
if err := credState.RotateCreds(ctx); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogState, err := newAwsCatalogPersistedState(
|
|
|
|
|
append([]awsCatalogPersistedStateOption{
|
|
|
|
|
withCredentials(credState),
|
|
|
|
|
}, p.testCatalogStateOpts...)...,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error setting up persisted state: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
// perform dry run to ensure we can interact with AWS as expected.
|
2024-10-28 11:50:18 -07:00
|
|
|
opts := []ec2Option{}
|
|
|
|
|
if catalogAttributes.DualStack {
|
|
|
|
|
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
|
|
|
|
}
|
|
|
|
|
if st := dryRunValidation(ctx, catalogState, opts); st != nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, st.Err()
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
persistedProto, err := catalogState.toProto()
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error converting persisted state to proto: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &pb.OnCreateCatalogResponse{
|
|
|
|
|
Persisted: persistedProto,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OnUpdateCatalog is called when a dynamic host catalog is updated.
|
|
|
|
|
func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalogRequest) (*pb.OnUpdateCatalogResponse, error) {
|
|
|
|
|
// For host catalogs we only need to really validate what is
|
|
|
|
|
// currently in the new copy of the host catalog data, so skip
|
|
|
|
|
// fetching the new copy to keep things a little less messy.
|
2024-09-12 16:46:55 +01:00
|
|
|
currentCatalog := req.GetCurrentCatalog()
|
|
|
|
|
if currentCatalog == nil {
|
|
|
|
|
return nil, errors.BadRequestStatus("current catalog is required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
2024-09-12 16:46:55 +01:00
|
|
|
newCatalog := req.GetNewCatalog()
|
|
|
|
|
if newCatalog == nil {
|
|
|
|
|
return nil, errors.BadRequestStatus("new catalog is required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
oldAttrs := currentCatalog.GetAttributes()
|
|
|
|
|
if oldAttrs == nil {
|
|
|
|
|
return nil, errors.BadRequestStatus("current catalog attributes are required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
2024-09-12 16:46:55 +01:00
|
|
|
oldCatalogAttributes, err := getCatalogAttributes(oldAttrs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
newAttrs := newCatalog.GetAttributes()
|
|
|
|
|
if newAttrs == nil {
|
|
|
|
|
return nil, errors.BadRequestStatus("new catalog attributes are required")
|
|
|
|
|
}
|
|
|
|
|
newCatalogAttributes, err := getCatalogAttributes(newAttrs)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
|
|
|
|
req.GetPersisted().GetSecrets(),
|
2024-09-12 16:46:55 +01:00
|
|
|
oldCatalogAttributes.CredentialAttributes,
|
2024-10-28 11:50:18 -07:00
|
|
|
oldCatalogAttributes.DualStack,
|
2023-07-18 16:33:24 -07:00
|
|
|
p.testCredStateOpts...)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
// Verify the incoming credentials are valid and return any errors to the
|
|
|
|
|
// user if they're not. Note this doesn't validate the credentials against
|
|
|
|
|
// AWS - it only does logical validation on the fields.
|
2024-10-28 11:50:18 -07:00
|
|
|
updatedCredentials, err := credential.GetCredentialsConfig(newCatalog.GetSecrets(), newCatalogAttributes.CredentialAttributes, newCatalogAttributes.DualStack)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, err
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
// We will be updating credentials when one of two changes occur:
|
|
|
|
|
// 1. the new catalog has secrets for static credentials, in which case we need to delete the old static credentials
|
|
|
|
|
// 2. the new catalog has a different roleARN value for dynamic credentials
|
|
|
|
|
if newCatalog.GetSecrets() != nil || newCatalogAttributes.RoleArn != oldCatalogAttributes.RoleArn {
|
|
|
|
|
// Ensure the incoming credentials are valid for interaction with the S3
|
|
|
|
|
// Bucket before replacing them.
|
|
|
|
|
newCredState, err := credential.NewAwsCredentialPersistedState(
|
|
|
|
|
append([]credential.AwsCredentialPersistedStateOption{
|
|
|
|
|
credential.WithCredentialsConfig(updatedCredentials),
|
|
|
|
|
}, p.testCredStateOpts...)...,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error setting up new credential persisted state: %s", err)
|
2024-09-12 16:46:55 +01:00
|
|
|
}
|
|
|
|
|
newCatalogState, err := newAwsCatalogPersistedState(
|
|
|
|
|
append([]awsCatalogPersistedStateOption{
|
|
|
|
|
withCredentials(newCredState),
|
|
|
|
|
}, p.testCatalogStateOpts...)...,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
2024-09-12 16:46:55 +01:00
|
|
|
}
|
2024-10-28 11:50:18 -07:00
|
|
|
opts := []ec2Option{}
|
|
|
|
|
if newCatalogAttributes.DualStack {
|
|
|
|
|
opts = append(opts, WithDualStack(newCatalogAttributes.DualStack))
|
|
|
|
|
}
|
|
|
|
|
if st := dryRunValidation(ctx, newCatalogState, opts); st != nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, st.Err()
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
// Replace the existing credential state.
|
|
|
|
|
// This checks the timestamp on the last rotation time as well
|
|
|
|
|
// and deletes the credentials if we are managing them
|
|
|
|
|
// (ie: if we've rotated them before).
|
|
|
|
|
if err := credState.ReplaceCreds(ctx, updatedCredentials); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-18 16:33:24 -07:00
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
if credential.GetCredentialType(credState.CredentialsConfig) == credential.StaticAWS {
|
|
|
|
|
// This is a validate check to make sure that we aren't disabling
|
|
|
|
|
// rotation for credentials currently being managed by rotation.
|
|
|
|
|
// This is not allowed.
|
|
|
|
|
if newCatalogAttributes.DisableCredentialRotation && newCatalog.GetSecrets() == nil {
|
|
|
|
|
if !credState.CredsLastRotatedTime.IsZero() {
|
|
|
|
|
return nil, errors.BadRequestStatus("cannot disable rotation for already-rotated credentials")
|
2023-07-18 16:33:24 -07:00
|
|
|
}
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
// If we're enabling rotation now but didn't before, or have
|
|
|
|
|
// freshly replaced credentials, we can rotate here.
|
|
|
|
|
if !newCatalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
2023-09-22 12:26:08 -07:00
|
|
|
if err := credState.RotateCreds(ctx); err != nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, err
|
2023-07-18 16:33:24 -07:00
|
|
|
}
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
catalogState, err := newAwsCatalogPersistedState(
|
|
|
|
|
append([]awsCatalogPersistedStateOption{
|
|
|
|
|
withCredentials(credState),
|
|
|
|
|
}, p.testCatalogStateOpts...)...,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
2024-09-12 16:46:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// perform dry run to ensure we can interact with AWS as expected.
|
2024-10-28 11:50:18 -07:00
|
|
|
opts := []ec2Option{}
|
|
|
|
|
if newCatalogAttributes.DualStack {
|
|
|
|
|
opts = append(opts, WithDualStack(newCatalogAttributes.DualStack))
|
|
|
|
|
}
|
|
|
|
|
if st := dryRunValidation(ctx, catalogState, opts); st != nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, st.Err()
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-20 11:02:17 -07:00
|
|
|
persistedProto, err := catalogState.toProto()
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error converting persisted state to proto: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &pb.OnUpdateCatalogResponse{
|
|
|
|
|
Persisted: persistedProto,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OnDeleteCatalog is called when a dynamic host catalog is deleted.
|
|
|
|
|
func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalogRequest) (*pb.OnDeleteCatalogResponse, error) {
|
|
|
|
|
catalog := req.GetCatalog()
|
|
|
|
|
if catalog == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("catalog is required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attrs := catalog.GetAttributes()
|
|
|
|
|
if attrs == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("catalog attributes are required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogAttributes, err := getCatalogAttributes(attrs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the persisted data.
|
|
|
|
|
// NOTE: We return on error here, blocking the delete. This may or
|
|
|
|
|
// may not be an overzealous approach to maintaining database/state
|
|
|
|
|
// integrity. May need to be changed at later time if there are
|
|
|
|
|
// scenarios where we might be deleting things and any secret state
|
|
|
|
|
// may be corrupt/and or legitimately missing.
|
|
|
|
|
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
|
|
|
|
req.GetPersisted().GetSecrets(),
|
2023-07-18 16:33:24 -07:00
|
|
|
catalogAttributes.CredentialAttributes,
|
2024-10-28 11:50:18 -07:00
|
|
|
catalogAttributes.DualStack,
|
2023-07-18 16:33:24 -07:00
|
|
|
p.testCredStateOpts...)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = newAwsCatalogPersistedState(
|
|
|
|
|
append([]awsCatalogPersistedStateOption{
|
|
|
|
|
withCredentials(credState),
|
|
|
|
|
}, p.testCatalogStateOpts...)...,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
2023-07-18 16:33:24 -07:00
|
|
|
// try to delete static credentials
|
2024-09-12 16:46:55 +01:00
|
|
|
if credential.GetCredentialType(credState.CredentialsConfig) == credential.StaticAWS {
|
2023-07-18 16:33:24 -07:00
|
|
|
if !credState.CredsLastRotatedTime.IsZero() {
|
|
|
|
|
// Delete old/existing credentials. This is done with the same
|
|
|
|
|
// credentials to ensure that it has the proper permissions to do
|
|
|
|
|
// it.
|
2023-09-22 12:26:08 -07:00
|
|
|
if err := credState.DeleteCreds(ctx); err != nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, err
|
2023-07-18 16:33:24 -07:00
|
|
|
}
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &pb.OnDeleteCatalogResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NormalizeSetData currently ensures that "filters" is an array value, even
|
|
|
|
|
// though it's accepted as a string value for CLI UX reasons
|
|
|
|
|
func (p *HostPlugin) NormalizeSetData(ctx context.Context, req *pb.NormalizeSetDataRequest) (*pb.NormalizeSetDataResponse, error) {
|
|
|
|
|
if req.Attributes == nil {
|
|
|
|
|
return new(pb.NormalizeSetDataResponse), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val := req.Attributes.Fields["filters"]
|
|
|
|
|
if val == nil {
|
|
|
|
|
return &pb.NormalizeSetDataResponse{Attributes: req.Attributes}, nil
|
|
|
|
|
}
|
|
|
|
|
stringVal, ok := val.Kind.(*structpb.Value_StringValue)
|
|
|
|
|
if !ok {
|
|
|
|
|
return &pb.NormalizeSetDataResponse{Attributes: req.Attributes}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
retAttrs := proto.Clone(req.Attributes).(*structpb.Struct)
|
|
|
|
|
retAttrs.Fields["filters"] = structpb.NewListValue(
|
|
|
|
|
&structpb.ListValue{
|
|
|
|
|
Values: []*structpb.Value{
|
|
|
|
|
structpb.NewStringValue(stringVal.StringValue),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return &pb.NormalizeSetDataResponse{Attributes: retAttrs}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OnCreateSet is called when a dynamic host set is created.
|
|
|
|
|
func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) {
|
|
|
|
|
catalog := req.GetCatalog()
|
|
|
|
|
if catalog == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("catalog is required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogAttrsRaw := catalog.GetAttributes()
|
|
|
|
|
if catalogAttrsRaw == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("catalog attributes are required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
|
|
|
|
req.GetPersisted().GetSecrets(),
|
2023-07-18 16:33:24 -07:00
|
|
|
catalogAttributes.CredentialAttributes,
|
2024-10-28 11:50:18 -07:00
|
|
|
catalogAttributes.DualStack,
|
2023-07-18 16:33:24 -07:00
|
|
|
p.testCredStateOpts...)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogState, err := newAwsCatalogPersistedState(
|
|
|
|
|
append([]awsCatalogPersistedStateOption{
|
|
|
|
|
withCredentials(credState),
|
|
|
|
|
}, p.testCatalogStateOpts...)...,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set := req.GetSet()
|
|
|
|
|
if set == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("set is required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if set.GetAttributes() == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("set attributes are required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
setAttrs, err := getSetAttributes(set.GetAttributes())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
describeInstanceFilters, err := buildFilters(setAttrs)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error building set filters: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-28 11:50:18 -07:00
|
|
|
opts := []ec2Option{}
|
|
|
|
|
if catalogAttributes.DualStack {
|
|
|
|
|
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
|
|
|
|
}
|
|
|
|
|
if st := dryRunValidation(ctx, catalogState, opts, describeInstanceFilters...); st != nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, st.Err()
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
2024-09-12 16:46:55 +01:00
|
|
|
return &pb.OnCreateSetResponse{}, nil
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OnUpdateSet is called when a dynamic host set is updated.
|
|
|
|
|
func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) {
|
|
|
|
|
catalog := req.GetCatalog()
|
|
|
|
|
if catalog == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("catalog is required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogAttrsRaw := catalog.GetAttributes()
|
|
|
|
|
if catalogAttrsRaw == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("catalog attributes are required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
|
|
|
|
req.GetPersisted().GetSecrets(),
|
2023-07-18 16:33:24 -07:00
|
|
|
catalogAttributes.CredentialAttributes,
|
2024-10-28 11:50:18 -07:00
|
|
|
catalogAttributes.DualStack,
|
2023-07-18 16:33:24 -07:00
|
|
|
p.testCredStateOpts...)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogState, err := newAwsCatalogPersistedState(
|
|
|
|
|
append([]awsCatalogPersistedStateOption{
|
|
|
|
|
withCredentials(credState),
|
|
|
|
|
}, p.testCatalogStateOpts...)...,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// As with catalog, we don't need to really look at the old host
|
|
|
|
|
// set here, just need to work off of/validate the new set config
|
|
|
|
|
set := req.GetNewSet()
|
|
|
|
|
if set == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("new set is required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if set.GetAttributes() == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("new set attributes are required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
setAttrs, err := getSetAttributes(set.GetAttributes())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
describeInstanceFilters, err := buildFilters(setAttrs)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error building set filters: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-28 11:50:18 -07:00
|
|
|
opts := []ec2Option{}
|
|
|
|
|
if catalogAttributes.DualStack {
|
|
|
|
|
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
|
|
|
|
}
|
|
|
|
|
if st := dryRunValidation(ctx, catalogState, opts, describeInstanceFilters...); st != nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, st.Err()
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
return &pb.OnUpdateSetResponse{}, nil
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OnDeleteSet is called when a dynamic host set is deleted.
|
|
|
|
|
func (p *HostPlugin) OnDeleteSet(ctx context.Context, req *pb.OnDeleteSetRequest) (*pb.OnDeleteSetResponse, error) {
|
|
|
|
|
// No-op, AWS host set does not maintain anything that requires
|
|
|
|
|
// cleanup
|
|
|
|
|
return &pb.OnDeleteSetResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListHosts returns the list of ec2 hosts and their descriptions.
|
|
|
|
|
func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) {
|
|
|
|
|
catalog := req.GetCatalog()
|
|
|
|
|
if catalog == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("catalog is required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogAttrsRaw := catalog.GetAttributes()
|
|
|
|
|
if catalogAttrsRaw == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("catalog attributes are required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-18 16:33:24 -07:00
|
|
|
credState, err := credential.AwsCredentialPersistedStateFromProto(
|
|
|
|
|
req.GetPersisted().GetSecrets(),
|
|
|
|
|
catalogAttributes.CredentialAttributes,
|
2024-10-28 11:50:18 -07:00
|
|
|
catalogAttributes.DualStack,
|
2023-07-18 16:33:24 -07:00
|
|
|
p.testCredStateOpts...)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catalogState, err := newAwsCatalogPersistedState(
|
|
|
|
|
append([]awsCatalogPersistedStateOption{
|
|
|
|
|
withCredentials(credState),
|
|
|
|
|
}, p.testCatalogStateOpts...)...,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error loading persisted state: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sets := req.GetSets()
|
|
|
|
|
if sets == nil {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("sets are required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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))
|
|
|
|
|
for i, set := range sets {
|
|
|
|
|
// Validate Id since we use it in output
|
|
|
|
|
if set.GetId() == "" {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, errors.BadRequestStatus("set id is required")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if set.GetAttributes() == nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("set %s attributes are required", set.GetId())
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
setAttrs, err := getSetAttributes(set.GetAttributes())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input, err := buildDescribeInstancesInput(setAttrs, false)
|
|
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error building DescribeInstances parameters for host set id %q: %s", set.GetId(), err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
queries[i] = hostSetQuery{
|
|
|
|
|
Id: set.GetId(),
|
|
|
|
|
Input: input,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-28 11:50:18 -07:00
|
|
|
opts := []ec2Option{}
|
|
|
|
|
if catalogAttributes.DualStack {
|
|
|
|
|
opts = append(opts, WithDualStack(catalogAttributes.DualStack))
|
|
|
|
|
}
|
|
|
|
|
ec2Client, err := catalogState.EC2Client(ctx, opts...)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error getting EC2 client: %s", err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run all queries now and assemble output.
|
|
|
|
|
var maxLen int
|
|
|
|
|
for i, query := range queries {
|
2023-07-04 11:03:24 -07:00
|
|
|
output, err := ec2Client.DescribeInstances(ctx, query.Input)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error running DescribeInstances for host set id %q: %s", query.Id, err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
queries[i].Output = output
|
|
|
|
|
|
|
|
|
|
// Process the output here, we will normalize this into a single
|
|
|
|
|
// set of hosts afterwards (possibly removing duplicates).
|
|
|
|
|
for _, reservation := range output.Reservations {
|
|
|
|
|
for _, instance := range reservation.Instances {
|
2026-05-14 20:57:48 -07:00
|
|
|
host, err := awsInstanceToHost(instance, catalogAttributes)
|
2023-03-20 11:02:17 -07:00
|
|
|
if err != nil {
|
2025-08-15 13:21:05 +05:30
|
|
|
return nil, errors.BadRequestStatusf("error processing host results for host set id %q: %s", query.Id, err)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
queries[i].OutputHosts = append(queries[i].OutputHosts, host)
|
|
|
|
|
maxLen++ // Increment maximum counter for allocation later
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now de-duplicate the hosts for the output. Maintain two sets:
|
|
|
|
|
// * A slice of hosts that will be used in the output
|
|
|
|
|
// * A map of hosts indexed by their external ID
|
|
|
|
|
//
|
|
|
|
|
// The map will is used in de-duplication to determine whether or
|
|
|
|
|
// not we've seen the host before to simply add the query's set ID
|
|
|
|
|
// to the list of set IDs that the host was seen in.
|
|
|
|
|
hostResultSlice := make([]*pb.ListHostsResponseHost, 0, maxLen)
|
|
|
|
|
hostResultMap := make(map[string]*pb.ListHostsResponseHost)
|
|
|
|
|
for _, query := range queries {
|
|
|
|
|
for _, host := range query.OutputHosts {
|
|
|
|
|
if existingHost, ok := hostResultMap[host.ExternalId]; ok {
|
|
|
|
|
// Existing host, just add the set ID to the list of seen IDs
|
|
|
|
|
// and continue
|
|
|
|
|
existingHost.SetIds = append(existingHost.SetIds, query.Id)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This will be the first seen entry, so append the set ID to
|
|
|
|
|
// this host, and add it.
|
|
|
|
|
host.SetIds = append(host.SetIds, query.Id)
|
|
|
|
|
hostResultSlice = append(hostResultSlice, host)
|
|
|
|
|
hostResultMap[host.ExternalId] = host
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Done!
|
|
|
|
|
return &pb.ListHostsResponse{
|
|
|
|
|
Hosts: hostResultSlice,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-04 11:03:24 -07:00
|
|
|
func buildFilters(attrs *SetAttributes) ([]types.Filter, error) {
|
|
|
|
|
var filters []types.Filter
|
2023-03-20 11:02:17 -07:00
|
|
|
var foundStateFilter bool
|
|
|
|
|
for _, filterAttr := range attrs.Filters {
|
|
|
|
|
// Each filter arrives in "k=v1,v2" format. First, split on equal sign.
|
|
|
|
|
splitFilter := strings.Split(filterAttr, "=")
|
|
|
|
|
switch {
|
|
|
|
|
case len(splitFilter) != 2:
|
|
|
|
|
return nil, fmt.Errorf("expected filter %q to contain a single equal sign", filterAttr)
|
|
|
|
|
case len(splitFilter[0]) == 0:
|
|
|
|
|
return nil, fmt.Errorf("filter %q contains an empty filter key", filterAttr)
|
|
|
|
|
case len(splitFilter[1]) == 0:
|
|
|
|
|
return nil, fmt.Errorf("filter %q contains an empty value", filterAttr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filterKey, filterValue := splitFilter[0], splitFilter[1]
|
|
|
|
|
if filterKey == "instance-state-code" || filterKey == "instance-state-name" {
|
|
|
|
|
foundStateFilter = true
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-04 11:03:24 -07:00
|
|
|
filters = append(filters, types.Filter{
|
2023-03-20 11:02:17 -07:00
|
|
|
Name: aws.String(filterKey),
|
2023-07-04 11:03:24 -07:00
|
|
|
Values: strings.Split(filterValue, ","),
|
2023-03-20 11:02:17 -07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !foundStateFilter {
|
|
|
|
|
// if there is no explicit instance state filter, add the
|
|
|
|
|
// instance-state-name = ["running"] to the filter set. This
|
|
|
|
|
// ensures that we filter on running instances only at the API
|
|
|
|
|
// side, saving time when processing results.
|
2023-07-04 11:03:24 -07:00
|
|
|
filters = append(filters, types.Filter{
|
2023-03-20 11:02:17 -07:00
|
|
|
Name: aws.String("instance-state-name"),
|
2023-07-04 11:03:24 -07:00
|
|
|
Values: []string{string(types.InstanceStateNameRunning)},
|
2023-03-20 11:02:17 -07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return filters, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.DescribeInstancesInput, error) {
|
|
|
|
|
filters, err := buildFilters(attrs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error building filters: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &ec2.DescribeInstancesInput{
|
|
|
|
|
DryRun: aws.Bool(dryRun),
|
|
|
|
|
Filters: filters,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// awsInstanceToHost processes data from an ec2.Instance and returns
|
|
|
|
|
// a ListHostsResponseHost with the ID and network addresses
|
|
|
|
|
// populated.
|
2026-05-14 20:57:48 -07:00
|
|
|
func awsInstanceToHost(instance types.Instance, catalogAttributes *CatalogAttributes) (*pb.ListHostsResponseHost, error) {
|
2023-07-04 11:03:24 -07:00
|
|
|
if aws.ToString(instance.InstanceId) == "" {
|
2024-09-12 16:46:55 +01:00
|
|
|
return nil, fmt.Errorf("response integrity error: missing instance id")
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
2026-05-14 20:57:48 -07:00
|
|
|
if catalogAttributes == nil {
|
|
|
|
|
return nil, fmt.Errorf("response integrity error: missing catalog attributes")
|
|
|
|
|
}
|
2023-03-20 11:02:17 -07:00
|
|
|
|
|
|
|
|
result := new(pb.ListHostsResponseHost)
|
|
|
|
|
|
|
|
|
|
// External ID is the instance ID.
|
2023-07-04 11:03:24 -07:00
|
|
|
result.ExternalId = aws.ToString(instance.InstanceId)
|
2023-03-20 11:02:17 -07:00
|
|
|
|
|
|
|
|
// External Name is the AWS "Name" tag, if it exists.
|
|
|
|
|
for _, t := range instance.Tags {
|
2023-07-04 11:03:24 -07:00
|
|
|
if aws.ToString(t.Key) == ConstInstanceNameTagKey {
|
|
|
|
|
result.ExternalName = aws.ToString(t.Value)
|
2023-03-20 11:02:17 -07:00
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 11:59:35 -07:00
|
|
|
// First let's get the instance's Private IPv4/DNS, Public IPv4/DNS, and IPv6 addresses
|
2026-05-14 20:57:48 -07:00
|
|
|
result.IpAddresses = appendDistinct(result.IpAddresses, instance.PrivateIpAddress, instance.PublicIpAddress, instance.Ipv6Address)
|
2023-07-04 11:03:24 -07:00
|
|
|
result.DnsNames = appendDistinct(result.DnsNames, instance.PrivateDnsName, instance.PublicDnsName)
|
2023-03-20 11:02:17 -07:00
|
|
|
|
2026-05-14 20:57:48 -07:00
|
|
|
if catalogAttributes.InstanceAddressesOnly {
|
|
|
|
|
// return early as we already have the instance addresses
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now go through all of the interfaces and sync the IP address of
|
2023-03-20 11:02:17 -07:00
|
|
|
// every interface.
|
|
|
|
|
for _, iface := range instance.NetworkInterfaces {
|
|
|
|
|
// Populate default IP addresses/DNS name similar to how we do
|
|
|
|
|
// for the entire instance.
|
2023-07-04 11:03:24 -07:00
|
|
|
result.IpAddresses = appendDistinct(result.IpAddresses, iface.PrivateIpAddress)
|
|
|
|
|
result.DnsNames = appendDistinct(result.DnsNames, iface.PrivateDnsName)
|
2023-03-20 11:02:17 -07:00
|
|
|
|
|
|
|
|
// Iterate through the private IP addresses and log the
|
|
|
|
|
// information.
|
|
|
|
|
for _, addr := range iface.PrivateIpAddresses {
|
2023-07-04 11:03:24 -07:00
|
|
|
result.IpAddresses = appendDistinct(result.IpAddresses, addr.PrivateIpAddress)
|
|
|
|
|
result.DnsNames = appendDistinct(result.DnsNames, addr.PrivateDnsName)
|
|
|
|
|
if addr.Association != nil {
|
|
|
|
|
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Association.PublicIp)
|
|
|
|
|
result.DnsNames = appendDistinct(result.DnsNames, addr.Association.PublicDnsName)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add the IPv6 addresses.
|
|
|
|
|
for _, addr := range iface.Ipv6Addresses {
|
2023-07-04 11:03:24 -07:00
|
|
|
result.IpAddresses = appendDistinct(result.IpAddresses, addr.Ipv6Address)
|
2023-03-20 11:02:17 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Done
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-12 16:46:55 +01:00
|
|
|
// dryRunValidation performs an AWS DescribeInstances call to verify the state's
|
|
|
|
|
// credentials, the host listing functionality as well as the filters, if any
|
|
|
|
|
// are passed in. This function can therefore be used for both host catalog and
|
|
|
|
|
// host set validation.
|
2024-10-28 11:50:18 -07:00
|
|
|
func dryRunValidation(ctx context.Context, state *awsCatalogPersistedState, ec2Opts []ec2Option, filters ...types.Filter) *status.Status {
|
2024-09-12 16:46:55 +01:00
|
|
|
if state == nil {
|
|
|
|
|
return status.New(codes.InvalidArgument, "persisted state is required")
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-28 11:50:18 -07:00
|
|
|
ec2Client, err := state.EC2Client(ctx, ec2Opts...)
|
2024-09-12 16:46:55 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return status.New(codes.InvalidArgument, fmt.Sprintf("error getting EC2 client: %s", err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = ec2Client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{Filters: filters})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return status.New(codes.FailedPrecondition, fmt.Sprintf("aws describe instances failed: %s", err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-04 11:03:24 -07:00
|
|
|
// appendDistinct will append the elements to the slice
|
|
|
|
|
// if an element is not nil, empty, and does not exist in slice.
|
|
|
|
|
func appendDistinct(slice []string, elems ...*string) []string {
|
|
|
|
|
for _, e := range elems {
|
|
|
|
|
value := aws.ToString(e)
|
|
|
|
|
if value == "" || stringInSlice(slice, value) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
slice = append(slice, value)
|
|
|
|
|
}
|
|
|
|
|
return slice
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// stringInSlice returns true if the slice contains the given element.
|
2023-03-20 11:02:17 -07:00
|
|
|
func stringInSlice(s []string, x string) bool {
|
|
|
|
|
for _, y := range s {
|
|
|
|
|
if x == y {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|