Skip to content

(aws-pipes-alpha): Incorrect usage of interfaces causes deadLetterTarget prop to be ignored #34812

Open
@jgfidelis

Description

@jgfidelis

Describe the bug

Hi, we use a library with its own set of constructs that copy regular constructs.

Lets say we have our own CustomSQS that implements IQueue interface.

Then I create a DynamoDBSource from @aws-cdk/aws-pipes-sources-alpha

new DynamoDBSource(myStream, {
      startingPosition: 'TRIM-HORIZON',
      deadLetterTarget: myCustomQueue,
    })

This builds since myCustomQueue implements IQueue.

But in the end the deadLetterTarget is ignored and does not show in template.

Reason are these line of code here

If your class receives a prop of type "IQueue", then it is incorrect to test if (object instanceof Queue)

What you can do is something like:

protected getDeadLetterTargetArn(deadLetterTarget?: IQueue | ITopic): string | undefined {
  if (isIQueue(deadLetterTarget)) {
    return deadLetterTarget.queueArn;
  } else if (isITopic(deadLetterTarget)) {
    return deadLetterTarget.topicArn;
  }
  return undefined;
}

function isIQueue(x: any): x is IQueue {
  return x && typeof x.queueArn === 'string';
}

function isITopic(x: any): x is ITopic {
  return x && typeof x.topicArn === 'string';
}

Regression Issue

  • Select this option if this issue appears to be a regression.

Last Known Working CDK Library Version

No response

Expected Behavior

If I pass an object with a class that implements IQueue, I should get a deadLetterTarget in template

Current Behavior

If I pass an object with a class that implements IQueue but is not a Queue, deadLetterTarget is null

Reproduction Steps

  • Create a custom queue class
import { Construct } from 'constructs';
import { IQueue, QueueEncryption } from 'aws-cdk-lib/aws-sqs';
import { IResource, Resource } from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';
import { Grant, IGrantable } from 'aws-cdk-lib/aws-iam';

interface CustomQueueProps {
  readonly queueArn: string;
  readonly queueUrl: string;
  readonly queueName: string;
  readonly fifo?: boolean;
  readonly encryptionType?: QueueEncryption;
  readonly encryptionMasterKey?: kms.IKey;
}

export class CustomQueue extends Resource implements IQueue {
  public readonly queueArn: string;
  public readonly queueUrl: string;
  public readonly queueName: string;
  public readonly fifo: boolean;
  public readonly encryptionType?: QueueEncryption;
  public readonly encryptionMasterKey?: kms.IKey;

  constructor(scope: Construct, id: string, props: CustomQueueProps) {
    super(scope, id);

    this.queueArn = props.queueArn;
    this.queueUrl = props.queueUrl;
    this.queueName = props.queueName;
    this.fifo = props.fifo ?? false;
    this.encryptionType = props.encryptionType;
    this.encryptionMasterKey = props.encryptionMasterKey;
  }

  addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult {
    // For mock/custom queues, you can either return a dummy result or implement actual logic
    return {
      statementAdded: false,
      policyDependable: this,
    };
  }

  grantConsumeMessages(grantee: IGrantable): Grant {
    return this.grant(grantee,
      'sqs:ChangeMessageVisibility',
      'sqs:DeleteMessage',
      'sqs:ReceiveMessage',
      'sqs:GetQueueAttributes',
      'sqs:GetQueueUrl',
    );
  }

  grantSendMessages(grantee: IGrantable): Grant {
    return this.grant(grantee,
      'sqs:SendMessage',
      'sqs:GetQueueAttributes',
      'sqs:GetQueueUrl',
    );
  }

  grantPurge(grantee: IGrantable): Grant {
    return this.grant(grantee,
      'sqs:PurgeQueue',
      'sqs:GetQueueAttributes',
      'sqs:GetQueueUrl',
    );
  }

  grant(grantee: IGrantable, ...queueActions: string[]): Grant {
    return Grant.addToPrincipal({
      grantee,
      actions: queueActions,
      resourceArns: [this.queueArn],
    });
  }
}
  • Then do something like:
const ddbTable = new TableV2(this, "DDBTable", {
  tableName: "ddb-table",
  partitionKey: {
    name: "Location",
    type: AttributeType.STRING,
  },
  dynamoStream: StreamViewType.NEW_AND_OLD_IMAGES,
  removalPolicy: RemovalPolicy.DESTROY,
});

const queue = new CustomQueue(this, 'MyCustomQueue', {
  queueArn: 'arn:aws:sqs:us-east-1:123456789012:MyQueue',
  queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue',
  queueName: 'MyQueue',
  fifo: false,
});
const pipeSource = new DynamoDBSource(ddbTable, {
  startingPosition: DynamoDBStartingPosition.LATEST,
  batchSize: 1,
  maximumRetryAttempts: 0,
  deadLetterTarget: queue,
});

new Pipe(this, "Pipe", {
  source: pipeSource,
  target: anotherSqs,
});

Check produced template after build:
DynamoDBStreamParameters won't have a dead letter target.

Possible Solution

protected getDeadLetterTargetArn(deadLetterTarget?: IQueue | ITopic): string | undefined {
  if (isIQueue(deadLetterTarget)) {
    return deadLetterTarget.queueArn;
  } else if (isITopic(deadLetterTarget)) {
    return deadLetterTarget.topicArn;
  }
  return undefined;
}

function isIQueue(x: any): x is IQueue {
  return x && typeof x.queueArn === 'string';
}

function isITopic(x: any): x is ITopic {
  return x && typeof x.topicArn === 'string';
}

Additional Information/Context

No response

AWS CDK Library version (aws-cdk-lib)

2.191.0-alpha.0

AWS CDK CLI version

2.174.0

Node.js Version

v16.20.2

OS

macOS Sequoia 15.4

Language

TypeScript

Language Version

No response

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    @aws-cdk/aws-sqsRelated to Amazon Simple Queue ServicebugThis issue is a bug.effort/mediumMedium work item – several days of effortp2

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions