package awsinv import ( "context" "errors" "fmt" "strings" "github.com/aws/aws-sdk-go-v2/aws" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/carlosframework/platform/internal/rootaws" ) // ErrExactInstance marks every result that fails to prove that the exact // enrolled EC2 instance is stopped or terminated. Callers must treat it as a // refusal, including when AWS says that the instance does not exist. var ErrExactInstance = errors.New("awsinv: exact EC2 instance is not fenced") // ExactInstance is the complete observation accepted by the declare-lost // client. Account comes from Reservation.OwnerId, not from a request field or // the caller's default profile. type ExactInstance struct { Account string `json:"account"` Region string `json:"region"` InstanceID string `json:"instance_id"` State string `json:"state"` RequestID string `json:"request_id"` } // ExactEC2API is the DescribeInstances slice of EC2API. The production EC2 // client and every existing awsinv EC2 fake satisfy it; keeping the verifier // on this one-method view makes a write-capable client unnecessary. type ExactEC2API interface { DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) } // NewEC2Reader resolves the operator's ordinary AWS credential chain and // returns the existing read-only EC2 seam in the enrolled region. It grants // no stop, terminate or restart operation. func NewEC2Reader(ctx context.Context, region string) (ExactEC2API, error) { if strings.TrimSpace(region) == "" { return nil, fmt.Errorf("%w: enrolled region is empty", ErrExactInstance) } cfg, err := rootaws.LoadDefaultConfig(ctx, awsconfig.WithRegion(region)) if err != nil { return nil, fmt.Errorf("%w: AWS config: %v", ErrExactInstance, err) } probeCtx, cancel := context.WithTimeout(ctx, credentialProbe) defer cancel() if _, err := cfg.Credentials.Retrieve(probeCtx); err != nil { return nil, fmt.Errorf("%w: no usable AWS credential: %v", ErrExactInstance, err) } return factoryFor(cfg)(region).EC2, nil } // VerifyExactStoppedInstance makes one DescribeInstances call constrained to // the exact enrolled ID. Any API error, pagination, missing field, duplicate // result, owner mismatch or live state refuses; InvalidInstanceID.NotFound is // therefore never treated as termination evidence. func VerifyExactStoppedInstance(ctx context.Context, api ExactEC2API, account, region, instanceID string) (ExactInstance, error) { wantAccount := strings.TrimSpace(account) wantRegion := strings.TrimSpace(region) wantID := strings.TrimSpace(instanceID) if api == nil || wantAccount == "" || wantRegion == "" || wantID == "" { return ExactInstance{}, fmt.Errorf("%w: incomplete enrolled cloud binding", ErrExactInstance) } out, err := api.DescribeInstances(ctx, &ec2.DescribeInstancesInput{InstanceIds: []string{wantID}}, func(o *ec2.Options) { o.Region = wantRegion }) if err != nil { return ExactInstance{}, fmt.Errorf("%w: DescribeInstances %s in %s/%s: %v", ErrExactInstance, wantID, wantAccount, wantRegion, err) } if out == nil || aws.ToString(out.NextToken) != "" || len(out.Reservations) != 1 { return ExactInstance{}, fmt.Errorf("%w: DescribeInstances returned a partial or ambiguous result", ErrExactInstance) } requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata) if !ok || strings.TrimSpace(requestID) == "" { return ExactInstance{}, ErrExactInstance } r := out.Reservations[0] owner := aws.ToString(r.OwnerId) if owner == "" || owner != wantAccount || len(r.Instances) != 1 { return ExactInstance{}, fmt.Errorf("%w: reservation owner or instance count does not match enrollment", ErrExactInstance) } in := r.Instances[0] gotID := aws.ToString(in.InstanceId) if gotID == "" || gotID != wantID || in.State == nil { return ExactInstance{}, fmt.Errorf("%w: returned instance does not exactly match enrollment", ErrExactInstance) } state := string(in.State.Name) if state != "stopped" && state != "terminated" { return ExactInstance{}, fmt.Errorf("%w: instance %s is %q", ErrExactInstance, wantID, state) } return ExactInstance{Account: owner, Region: wantRegion, InstanceID: gotID, State: state, RequestID: requestID}, nil }