diff --git a/apps/jsii-docgen/src/docgen/view/api-reference.ts b/apps/jsii-docgen/src/docgen/view/api-reference.ts index 8a408a03597..7d578422fef 100644 --- a/apps/jsii-docgen/src/docgen/view/api-reference.ts +++ b/apps/jsii-docgen/src/docgen/view/api-reference.ts @@ -78,7 +78,7 @@ export class ApiReference { this.constructs = new Constructs( transpile, classes, - this.searchableInterfaces(interfaces) + this.searchableInterfaces([...assembly.allInterfaces]) ); this.classes = new Classes(transpile, classes); this.structs = new Structs(transpile, interfaces); diff --git a/docs/docs/04-standard-library/aws/api-reference.md b/docs/docs/04-standard-library/aws/api-reference.md index d609e8a8b54..af9b67b7d52 100644 --- a/docs/docs/04-standard-library/aws/api-reference.md +++ b/docs/docs/04-standard-library/aws/api-reference.md @@ -146,6 +146,155 @@ The IAM certificate identifier value. --- +### QueueRef + +A reference to an external SQS queue. + +#### Initializers + +```wing +bring aws; + +new aws.QueueRef(queueArn: str); +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| queueArn | str | *No description.* | + +--- + +##### `queueArn`Required + +- *Type:* str + +--- + +#### Methods + +##### Inflight Methods + +| **Name** | **Description** | +| --- | --- | +| approxSize | Retrieve the approximate number of messages in the queue. | +| pop | Pop a message from the queue. | +| purge | Purge all of the messages in the queue. | +| push | Push one or more messages to the queue. | + +--- + +##### `approxSize` + +```wing +inflight approxSize(): num +``` + +Retrieve the approximate number of messages in the queue. + +##### `pop` + +```wing +inflight pop(): str +``` + +Pop a message from the queue. + +##### `purge` + +```wing +inflight purge(): void +``` + +Purge all of the messages in the queue. + +##### `push` + +```wing +inflight push(...messages: Array): void +``` + +Push one or more messages to the queue. + +###### `messages`Required + +- *Type:* str + +Payload to send to the queue. + +Each message must be non-empty. + +--- + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| onLiftType | A hook called by the Wing compiler once for each inflight host that needs to use this type inflight. | + +--- + +##### `onLiftType` + +```wing +bring aws; + +aws.QueueRef.onLiftType(host: IInflightHost, ops: MutArray); +``` + +A hook called by the Wing compiler once for each inflight host that needs to use this type inflight. + +The list of requested inflight methods +needed by the inflight host are given by `ops`. + +This method is commonly used for adding permissions, environment variables, or +other capabilities to the inflight host. + +###### `host`Required + +- *Type:* IInflightHost + +--- + +###### `ops`Required + +- *Type:* MutArray<str> + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| node | constructs.Node | The tree node. | +| queueArn | str | The ARN of this queue. | + +--- + +##### `node`Required + +```wing +node: Node; +``` + +- *Type:* constructs.Node + +The tree node. + +--- + +##### `queueArn`Required + +```wing +queueArn: str; +``` + +- *Type:* str + +The ARN of this queue. + +--- + + ## Classes ### Api diff --git a/docs/docs/04-standard-library/cloud/queue.md b/docs/docs/04-standard-library/cloud/queue.md index 43437098810..6ec262f096d 100644 --- a/docs/docs/04-standard-library/cloud/queue.md +++ b/docs/docs/04-standard-library/cloud/queue.md @@ -43,7 +43,7 @@ new cloud.Function(inflight () => { ### Using Queue inflight api -Pusing messages, popping them, and purge +Pushing messages, popping them, and purging. ```ts playground bring cloud; @@ -62,6 +62,30 @@ new cloud.Function(inflight () => { }); ``` +### Referencing an external queue + +If you would like to reference an existing queue from within your application you can use the +`QueueRef` classes in the target-specific namespaces. + +> This is currently only supported for `aws`. + +The following example defines a reference to an Amazon SQS queue with a specific ARN and sends a +message to the queue from the function: + +```js +bring aws; + +let outbox = new aws.QueueRef("arn:aws:sqs:us-east-1:111111111111:Outbox"); + +new cloud.Function(inflight () => { + outbox.push("send an email"); +}); +``` + +This works both when running in the simulator (requires AWS credentials on the developer's machine) +and when deployed to AWS. When this is deployed to AWS, the AWS Lambda IAM policy will include the +needed permissions. + ## Target-specific details ### Simulator (`sim`) diff --git a/docs/docs/04-standard-library/ui/api-reference.md b/docs/docs/04-standard-library/ui/api-reference.md index df5a171b82e..c5cd6c5b6c6 100644 --- a/docs/docs/04-standard-library/ui/api-reference.md +++ b/docs/docs/04-standard-library/ui/api-reference.md @@ -519,10 +519,24 @@ let FieldProps = ui.FieldProps{ ... }; | **Name** | **Type** | **Description** | | --- | --- | --- | +| link | bool | Indicates that this field is a link. | | refreshRate | duration | How often the field should be refreshed. | --- +##### `link`Optional + +```wing +link: bool; +``` + +- *Type:* bool +- *Default:* false + +Indicates that this field is a link. + +--- + ##### `refreshRate`Optional ```wing diff --git a/examples/tests/sdk_tests/queue/queue-ref.test.w b/examples/tests/sdk_tests/queue/queue-ref.test.w new file mode 100644 index 00000000000..fff25baefa4 --- /dev/null +++ b/examples/tests/sdk_tests/queue/queue-ref.test.w @@ -0,0 +1,54 @@ +bring cloud; +bring aws; +bring util; + +// a really cool way to test a "ref" resource: basically, we define a cloud.Queue +// and then we define a `QueueRef` that references it's ARN. nice, ha? + +let c = new cloud.Counter(); +let q = new cloud.Queue(); + +q.setConsumer(inflight () => { + c.inc(); +}); + +// this will only work if we are testing on tf-aws +if let arn = aws.Queue.from(q)?.queueArn { + let qref = new aws.QueueRef(arn); + + new cloud.Function(inflight () => { + qref.push("hello"); + qref.pop(); + }); + + test "can push to an external queue (QueueRef)" { + qref.push("m1"); + qref.push("m2"); + qref.push("m3"); + util.waitUntil(() => { + return c.peek() == 3; + }); + } +} + +if util.env("WING_TARGET") == "sim" { + bring expect; + + let dummyArn = "arn:aws:sqs:us-east-1:111111111111:Queue-11111111"; + let qr = new aws.QueueRef(dummyArn); + + test "queueArn returns the arn" { + expect.equal(dummyArn, qr.queueArn); + } + + test "push() sends a request to aws, fails because we are using a dummy queue" { + let var err = false; + try { + qr.push("foo"); + } catch e { + err = true; + } + + assert(err); + } +} \ No newline at end of file diff --git a/libs/wingsdk/src/cloud/queue.md b/libs/wingsdk/src/cloud/queue.md index 3bc93e71a7e..9e769410044 100644 --- a/libs/wingsdk/src/cloud/queue.md +++ b/libs/wingsdk/src/cloud/queue.md @@ -43,7 +43,7 @@ new cloud.Function(inflight () => { ### Using Queue inflight api -Pusing messages, popping them, and purge +Pushing messages, popping them, and purging. ```ts playground bring cloud; @@ -62,6 +62,30 @@ new cloud.Function(inflight () => { }); ``` +### Referencing an external queue + +If you would like to reference an existing queue from within your application you can use the +`QueueRef` classes in the target-specific namespaces. + +> This is currently only supported for `aws`. + +The following example defines a reference to an Amazon SQS queue with a specific ARN and sends a +message to the queue from the function: + +```js +bring aws; + +let outbox = new aws.QueueRef("arn:aws:sqs:us-east-1:111111111111:Outbox"); + +new cloud.Function(inflight () => { + outbox.push("send an email"); +}); +``` + +This works both when running in the simulator (requires AWS credentials on the developer's machine) +and when deployed to AWS. When this is deployed to AWS, the AWS Lambda IAM policy will include the +needed permissions. + ## Target-specific details ### Simulator (`sim`) diff --git a/libs/wingsdk/src/core/tree.ts b/libs/wingsdk/src/core/tree.ts index 806b8895b37..da46c05207c 100644 --- a/libs/wingsdk/src/core/tree.ts +++ b/libs/wingsdk/src/core/tree.ts @@ -93,6 +93,7 @@ export interface UIField { /** The construct path to a cloud.Function */ readonly handler: string; readonly refreshRate: number | undefined; + readonly link?: boolean; } /** @internal */ diff --git a/libs/wingsdk/src/shared-aws/permissions.ts b/libs/wingsdk/src/shared-aws/permissions.ts index 15edc2bb34d..3a281059189 100644 --- a/libs/wingsdk/src/shared-aws/permissions.ts +++ b/libs/wingsdk/src/shared-aws/permissions.ts @@ -24,6 +24,12 @@ export function calculateQueuePermissions( ): PolicyStatement[] { const policies: PolicyStatement[] = []; + // this is always needed in order to resolve URL from ARN/name + policies.push({ + actions: ["sqs:GetQueueUrl"], + resources: [arn], + }); + if (ops.includes(cloud.QueueInflightMethods.PUSH)) { policies.push({ actions: ["sqs:SendMessage"], diff --git a/libs/wingsdk/src/shared-aws/queue.inflight.ts b/libs/wingsdk/src/shared-aws/queue.inflight.ts index 243dc3aa772..431566da4c7 100644 --- a/libs/wingsdk/src/shared-aws/queue.inflight.ts +++ b/libs/wingsdk/src/shared-aws/queue.inflight.ts @@ -6,12 +6,15 @@ import { ReceiveMessageCommand, InvalidMessageContents, DeleteMessageCommand, + GetQueueUrlCommand, } from "@aws-sdk/client-sqs"; import { IQueueClient } from "../cloud"; export class QueueClient implements IQueueClient { + private _queueUrl?: string; + constructor( - private readonly queueUrl: string, + private readonly _queueUrlOrArn: string, private readonly client: SQSClient = new SQSClient({}) ) {} @@ -19,10 +22,11 @@ export class QueueClient implements IQueueClient { if (messages.includes("")) { throw new Error("Empty messages are not allowed"); } + const messagePromises = messages.map(async (message) => { try { const command = new SendMessageCommand({ - QueueUrl: this.queueUrl, + QueueUrl: await this.queueUrl(), MessageBody: message, }); await this.client.send(command); @@ -41,16 +45,46 @@ export class QueueClient implements IQueueClient { await Promise.all(messagePromises); } + public async queueUrl(): Promise { + if (!this._queueUrl) { + // if we have the queue name instead of the url, then we need to resolve it first + if (this._queueUrlOrArn.startsWith("https://")) { + this._queueUrl = this._queueUrlOrArn; + } else { + // extract the queue name from its ARN + const arnParts = this._queueUrlOrArn.split(":"); + const queueName = arnParts[arnParts.length - 1].split("/").pop(); + if (!queueName) { + throw new Error( + `Unable to extract queue name from ARN: ${this._queueUrlOrArn}` + ); + } + + const command = new GetQueueUrlCommand({ QueueName: queueName }); + const data = await this.client.send(command); + if (!data.QueueUrl) { + throw new Error( + `Unable to resolve queue URL from SQS queue ARN: ${this._queueUrlOrArn}` + ); + } + + this._queueUrl = data.QueueUrl; + } + } + + return this._queueUrl; + } + public async purge(): Promise { const command = new PurgeQueueCommand({ - QueueUrl: this.queueUrl, + QueueUrl: await this.queueUrl(), }); await this.client.send(command); } public async approxSize(): Promise { const command = new GetQueueAttributesCommand({ - QueueUrl: this.queueUrl, + QueueUrl: await this.queueUrl(), AttributeNames: ["ApproximateNumberOfMessages"], }); const data = await this.client.send(command); @@ -59,7 +93,7 @@ export class QueueClient implements IQueueClient { public async pop(): Promise { const receiveCommand = new ReceiveMessageCommand({ - QueueUrl: this.queueUrl, + QueueUrl: await this.queueUrl(), MaxNumberOfMessages: 1, }); const data = await this.client.send(receiveCommand); @@ -71,7 +105,7 @@ export class QueueClient implements IQueueClient { if (message.ReceiptHandle) { const deleteCommand = new DeleteMessageCommand({ - QueueUrl: this.queueUrl, + QueueUrl: await this.queueUrl(), ReceiptHandle: message.ReceiptHandle, }); await this.client.send(deleteCommand); diff --git a/libs/wingsdk/src/shared-aws/queue.ts b/libs/wingsdk/src/shared-aws/queue.ts index 047cae96dd5..a706fad0b3f 100644 --- a/libs/wingsdk/src/shared-aws/queue.ts +++ b/libs/wingsdk/src/shared-aws/queue.ts @@ -1,4 +1,14 @@ +import { Construct } from "constructs"; +import { Function } from "./function"; +import { calculateQueuePermissions } from "./permissions"; +import { isValidArn } from "./util"; import { cloud } from ".."; +import { IQueueClient } from "../cloud"; +import { InflightClient } from "../core"; +import { INFLIGHT_SYMBOL } from "../core/types"; +import { Testing } from "../simulator"; +import { IInflightHost, Node, Resource } from "../std"; +import * as ui from "../ui"; /** * A shared interface for AWS queues. @@ -20,6 +30,8 @@ export interface IAwsQueue { readonly queueUrl: string; } +const QUEUE_URL_METHOD = "queueUrl"; + /** * A helper class for working with AWS queues. */ @@ -44,3 +56,126 @@ export class Queue { ); } } + +/** + * A reference to an external SQS queue. + * + * @inflight `@winglang/sdk.cloud.IQueueClient` + */ +export class QueueRef extends Resource { + /** @internal */ + public [INFLIGHT_SYMBOL]?: IQueueClient; + + /** + * The ARN of this queue. + */ + public readonly queueArn: string; + + constructor(scope: Construct, id: string, queueArn: string) { + super(scope, id); + + if (!isValidArn(queueArn, "sqs")) { + throw new Error(`"${queueArn}" is not a valid Amazon SQS ARN`); + } + + this.queueArn = queueArn; + + this.addUserInterface(); + } + + public onLift(host: IInflightHost, ops: string[]): void { + // if this is an AWS function, add the necessary IAM permissions + const fn = Function.from(host); + if (fn) { + fn.addPolicyStatements(...calculateQueuePermissions(this.queueArn, ops)); + } + + host.addEnvironment(this.envName(), this.queueArn); + super.onLift(host, ops); + } + + /** @internal */ + public _toInflight(): string { + return InflightClient.for(__dirname, __filename, "QueueClient", [ + `process.env["${this.envName()}"]`, + ]); + } + + private envName(): string { + return `QUEUE_ARN_${this.node.addr.slice(-8)}`; + } + + /** @internal */ + public _supportedOps(): string[] { + return [ + QUEUE_URL_METHOD, // AWS-specific + cloud.QueueInflightMethods.PUSH, + cloud.QueueInflightMethods.PURGE, + cloud.QueueInflightMethods.APPROX_SIZE, + cloud.QueueInflightMethods.POP, + ]; + } + + private addUserInterface() { + Node.of(this).color = "pink"; + + const queueUrlHandler = Testing.makeHandler( + ` + async handle() { + try { + return await this.queue.queueUrl(); + } catch (e) { + return e.message; + } + } + `, + { + queue: { + obj: this, + ops: [QUEUE_URL_METHOD], + }, + } + ); + + new ui.Field(this, "QueueUrlField", "SQS Queue URL", queueUrlHandler, { + link: true, + }); + + const awsConsoleHandler = Testing.makeHandler( + `async handle() { + try { + const url = await this.queue.queueUrl(); + const x = new URL(url); + const region = x.hostname.split(".")[1]; + return "https://" + region + ".console.aws.amazon.com/sqs/v3/home?region=" + region + "#/queues/" + encodeURIComponent(url); + } catch (e) { + return e.message; + } + }`, + { + queue: { + obj: this, + ops: [QUEUE_URL_METHOD], + }, + } + ); + + new ui.Field(this, "AwsConsoleField", "AWS Console", awsConsoleHandler, { + link: true, + }); + + const queueArnHandler = Testing.makeHandler( + `async handle() { + return this.queueArn; + }`, + { + queueArn: { + obj: this.queueArn, + ops: [], + }, + } + ); + + new ui.Field(this, "QueueArnField", "SQS Queue ARN", queueArnHandler); + } +} diff --git a/libs/wingsdk/src/shared-aws/util.ts b/libs/wingsdk/src/shared-aws/util.ts new file mode 100644 index 00000000000..a2302ce6878 --- /dev/null +++ b/libs/wingsdk/src/shared-aws/util.ts @@ -0,0 +1,24 @@ +export function isValidArn(arn: string, service: string) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { Token } = require("cdktf"); + + // if the ARN is an unresolved token, we can't validate it so assume it's valid + if (Token.isUnresolved(arn)) { + return true; + } + + const parts = arn.split(":"); + if (parts.length !== 6) { + return false; + } + + if (parts[0] !== "arn") { + return false; + } + + if (parts[2] !== service) { + return false; + } + + return true; +} diff --git a/libs/wingsdk/src/ui/field.ts b/libs/wingsdk/src/ui/field.ts index 6be822a9d5e..4cfb4813026 100644 --- a/libs/wingsdk/src/ui/field.ts +++ b/libs/wingsdk/src/ui/field.ts @@ -19,6 +19,13 @@ export interface FieldProps { * @default - no automatic refresh */ readonly refreshRate?: Duration; + + /** + * Indicates that this field is a link. + * + * @default false + */ + readonly link?: boolean; } /** @@ -49,6 +56,7 @@ export class Field extends VisualComponent { private readonly fn: Function; private readonly label: string; private readonly refreshRate: number | undefined; + private readonly link: boolean | undefined; constructor( scope: Construct, @@ -62,6 +70,7 @@ export class Field extends VisualComponent { this.label = label; this.refreshRate = props.refreshRate?.seconds; this.fn = new Function(this, "Handler", handler); + this.link = props.link; } /** @internal */ @@ -71,6 +80,7 @@ export class Field extends VisualComponent { label: this.label, handler: this.fn.node.path, refreshRate: this.refreshRate, + link: this.link, }; } diff --git a/libs/wingsdk/test/shared-aws/queue.inflight.test.ts b/libs/wingsdk/test/shared-aws/queue.inflight.test.ts index 085dbc4665d..e2c783d3ab6 100644 --- a/libs/wingsdk/test/shared-aws/queue.inflight.test.ts +++ b/libs/wingsdk/test/shared-aws/queue.inflight.test.ts @@ -7,12 +7,14 @@ import { InvalidMessageContents, DeleteMessageCommand, ReceiveMessageCommandOutput, + GetQueueUrlCommand, } from "@aws-sdk/client-sqs"; import { mockClient } from "aws-sdk-client-mock"; import { test, expect, beforeEach } from "vitest"; import { QueueClient } from "../../src/shared-aws/queue.inflight"; import "aws-sdk-client-mock-jest"; +const QUEUE_URL = "https://my-queue-url"; const sqsMock = mockClient(SQSClient); beforeEach(() => { @@ -21,7 +23,6 @@ beforeEach(() => { test("push - happy path", async () => { // GIVEN - const QUEUE_URL = "QUEUE_URL"; const MESSAGE = "MESSAGE"; const RESPONSE = { MessageId: "MESSAGE_ID", @@ -37,11 +38,11 @@ test("push - happy path", async () => { // THEN expect(response).toEqual(undefined); + expect(sqsMock).toHaveReceivedCommandTimes(SendMessageCommand, 1); }); test("push batch - happy path", async () => { // GIVEN - const QUEUE_URL = "QUEUE_URL"; const MESSAGES = ["MESSAGE1", "MESSAGE2", "MESSAGE3"]; const RESPONSE = { MessageId: "MESSAGE_ID", @@ -72,7 +73,6 @@ test("push batch - happy path", async () => { test("push - sad path invalid message", async () => { // GIVEN - const QUEUE_URL = "QUEUE_URL"; const MESSAGE = "INVALID_MESSAGE"; sqsMock @@ -95,7 +95,6 @@ test("push - sad path invalid message", async () => { test("push - sad path empty message", async () => { // GIVEN - const QUEUE_URL = "QUEUE_URL"; const MESSAGE = ""; // WHEN @@ -113,7 +112,6 @@ test("push - sad path empty message", async () => { test("push - sad path unknown error", async () => { // GIVEN - const QUEUE_URL = "QUEUE_URL"; const MESSAGE = "MESSAGE"; sqsMock @@ -131,7 +129,6 @@ test("push - sad path unknown error", async () => { test("purge - happy path", async () => { // GIVEN - const QUEUE_URL = "QUEUE_URL"; const RESPONSE = {}; sqsMock.on(PurgeQueueCommand, { QueueUrl: QUEUE_URL }).resolves(RESPONSE); @@ -147,7 +144,6 @@ test("purge - happy path", async () => { test("approxSize - happy path", async () => { // GIVEN const QUEUE_SIZE = 3; - const QUEUE_URL = "QUEUE_URL"; const GET_QUEUE_ATTRIBUTES_RESPONSE = { Attributes: { ApproximateNumberOfMessages: QUEUE_SIZE.toString() }, }; @@ -166,7 +162,6 @@ test("approxSize - happy path", async () => { test("pop - happy path", async () => { // GIVEN - const QUEUE_URL = "QUEUE_URL"; const MESSAGE = "MESSAGE"; const ONE_MSG_RESPONSE: ReceiveMessageCommandOutput = { Messages: [ @@ -198,7 +193,6 @@ test("pop - happy path", async () => { test("pop - happy path w/o message receipt", async () => { // GIVEN - const QUEUE_URL = "QUEUE_URL"; const MESSAGE = "MESSAGE"; const ONE_MSG_RESPONSE: ReceiveMessageCommandOutput = { Messages: [ @@ -230,7 +224,6 @@ test("pop - happy path w/o message receipt", async () => { test("pop - happy path w/ no message in the queue", async () => { // GIVEN - const QUEUE_URL = "QUEUE_URL"; const NO_MSG_RESPONSE = {}; sqsMock @@ -248,3 +241,25 @@ test("pop - happy path w/ no message in the queue", async () => { expect(sqsMock).toHaveReceivedCommandTimes(ReceiveMessageCommand, 2); expect(sqsMock).toHaveReceivedCommandTimes(DeleteMessageCommand, 0); }); + +test("if a queue name is provided, the url is resolved", async () => { + const queueName = "MyQueueName"; + + // GIVEN + sqsMock.on(GetQueueUrlCommand, { QueueName: queueName }).resolves({ + QueueUrl: "https://my-queue-url", + }); + + sqsMock + .on(SendMessageCommand, { + QueueUrl: "https://my-queue-url", + MessageBody: "test", + }) + .resolves({}); + + const client = new QueueClient(queueName); + await client.push("test"); + + expect(sqsMock).toHaveReceivedCommandTimes(GetQueueUrlCommand, 1); + expect(sqsMock).toHaveReceivedCommandTimes(SendMessageCommand, 1); +}); diff --git a/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap b/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap index 41f30ea1238..ecf8f57cca0 100644 --- a/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap +++ b/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap @@ -199,7 +199,7 @@ exports[`function with a queue binding 3`] = ` }, "aws_iam_role_policy": { "Function_IamRolePolicy_E3B26607": { - "policy": "{"Version":"2012-10-17","Statement":[{"Action":["sqs:SendMessage"],"Resource":["\${aws_sqs_queue.Queue.arn}"],"Effect":"Allow"}]}", + "policy": "{"Version":"2012-10-17","Statement":[{"Action":["sqs:GetQueueUrl"],"Resource":["\${aws_sqs_queue.Queue.arn}"],"Effect":"Allow"},{"Action":["sqs:SendMessage"],"Resource":["\${aws_sqs_queue.Queue.arn}"],"Effect":"Allow"}]}", "role": "\${aws_iam_role.Function_IamRole_678BE84C.name}", }, "Queue-SetConsumer0_IamRolePolicy_0299B5AB": { diff --git a/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap b/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap index 6378f228d9a..28e841f6eb5 100644 --- a/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap +++ b/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap @@ -1,5 +1,627 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`QueueRef in an TFAWS app can be used to reference an existing queue 1`] = ` +{ + "resource": { + "aws_cloudwatch_log_group": { + "Function_CloudwatchLogGroup_ABDCF4C4": { + "name": "/aws/lambda/Function-c852aba6", + "retention_in_days": 30, + }, + "QueueRef_AwsConsoleField_Handler_CloudwatchLogGroup_1FAC3ED5": { + "name": "/aws/lambda/Handler-c85cfbef", + "retention_in_days": 30, + }, + "QueueRef_QueueArnField_Handler_CloudwatchLogGroup_BDFDF613": { + "name": "/aws/lambda/Handler-c8c3c90a", + "retention_in_days": 30, + }, + "QueueRef_QueueUrlField_Handler_CloudwatchLogGroup_8AB6E3DD": { + "name": "/aws/lambda/Handler-c8ec121e", + "retention_in_days": 30, + }, + }, + "aws_iam_role": { + "Function_IamRole_678BE84C": { + "assume_role_policy": "{"Version":"2012-10-17","Statement":[{"Action":"sts:AssumeRole","Principal":{"Service":"lambda.amazonaws.com"},"Effect":"Allow"}]}", + }, + "QueueRef_AwsConsoleField_Handler_IamRole_0DD0004C": { + "assume_role_policy": "{"Version":"2012-10-17","Statement":[{"Action":"sts:AssumeRole","Principal":{"Service":"lambda.amazonaws.com"},"Effect":"Allow"}]}", + }, + "QueueRef_QueueArnField_Handler_IamRole_C121BD5E": { + "assume_role_policy": "{"Version":"2012-10-17","Statement":[{"Action":"sts:AssumeRole","Principal":{"Service":"lambda.amazonaws.com"},"Effect":"Allow"}]}", + }, + "QueueRef_QueueUrlField_Handler_IamRole_709A15CB": { + "assume_role_policy": "{"Version":"2012-10-17","Statement":[{"Action":"sts:AssumeRole","Principal":{"Service":"lambda.amazonaws.com"},"Effect":"Allow"}]}", + }, + }, + "aws_iam_role_policy": { + "Function_IamRolePolicy_E3B26607": { + "policy": "{"Version":"2012-10-17","Statement":[{"Action":["sqs:GetQueueUrl"],"Resource":["arn:aws:sqs:us-west-2:123456789012:MyQueue1234"],"Effect":"Allow"},{"Action":["sqs:SendMessage"],"Resource":["arn:aws:sqs:us-west-2:123456789012:MyQueue1234"],"Effect":"Allow"},{"Action":["sqs:GetQueueAttributes"],"Resource":["arn:aws:sqs:us-west-2:123456789012:MyQueue1234"],"Effect":"Allow"}]}", + "role": "\${aws_iam_role.Function_IamRole_678BE84C.name}", + }, + "QueueRef_AwsConsoleField_Handler_IamRolePolicy_184A5238": { + "policy": "{"Version":"2012-10-17","Statement":[{"Action":["sqs:GetQueueUrl"],"Resource":["arn:aws:sqs:us-west-2:123456789012:MyQueue1234"],"Effect":"Allow"}]}", + "role": "\${aws_iam_role.QueueRef_AwsConsoleField_Handler_IamRole_0DD0004C.name}", + }, + "QueueRef_QueueArnField_Handler_IamRolePolicy_64EE3F4B": { + "policy": "{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"none:null","Resource":"*"}]}", + "role": "\${aws_iam_role.QueueRef_QueueArnField_Handler_IamRole_C121BD5E.name}", + }, + "QueueRef_QueueUrlField_Handler_IamRolePolicy_A9F2F773": { + "policy": "{"Version":"2012-10-17","Statement":[{"Action":["sqs:GetQueueUrl"],"Resource":["arn:aws:sqs:us-west-2:123456789012:MyQueue1234"],"Effect":"Allow"}]}", + "role": "\${aws_iam_role.QueueRef_QueueUrlField_Handler_IamRole_709A15CB.name}", + }, + }, + "aws_iam_role_policy_attachment": { + "Function_IamRolePolicyAttachment_CACE1358": { + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "\${aws_iam_role.Function_IamRole_678BE84C.name}", + }, + "QueueRef_AwsConsoleField_Handler_IamRolePolicyAttachment_1F892467": { + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "\${aws_iam_role.QueueRef_AwsConsoleField_Handler_IamRole_0DD0004C.name}", + }, + "QueueRef_QueueArnField_Handler_IamRolePolicyAttachment_BA9083B2": { + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "\${aws_iam_role.QueueRef_QueueArnField_Handler_IamRole_C121BD5E.name}", + }, + "QueueRef_QueueUrlField_Handler_IamRolePolicyAttachment_9FA1E52D": { + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "\${aws_iam_role.QueueRef_QueueUrlField_Handler_IamRole_709A15CB.name}", + }, + }, + "aws_lambda_function": { + "Function": { + "architectures": [ + "arm64", + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "QUEUE_ARN_23984aca": "arn:aws:sqs:us-west-2:123456789012:MyQueue1234", + "WING_FUNCTION_NAME": "Function-c852aba6", + }, + }, + "function_name": "Function-c852aba6", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "\${aws_iam_role.Function_IamRole_678BE84C.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "\${aws_s3_bucket.Code.bucket}", + "s3_key": "\${aws_s3_object.Function_S3Object_C62A0C2D.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [], + }, + }, + "QueueRef_AwsConsoleField_Handler_2D13D4BF": { + "architectures": [ + "arm64", + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "QUEUE_ARN_23984aca": "arn:aws:sqs:us-west-2:123456789012:MyQueue1234", + "WING_FUNCTION_NAME": "Handler-c85cfbef", + }, + }, + "function_name": "Handler-c85cfbef", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "\${aws_iam_role.QueueRef_AwsConsoleField_Handler_IamRole_0DD0004C.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "\${aws_s3_bucket.Code.bucket}", + "s3_key": "\${aws_s3_object.QueueRef_AwsConsoleField_Handler_S3Object_C4237D42.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [], + }, + }, + "QueueRef_QueueArnField_Handler_0B477E06": { + "architectures": [ + "arm64", + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "Handler-c8c3c90a", + }, + }, + "function_name": "Handler-c8c3c90a", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "\${aws_iam_role.QueueRef_QueueArnField_Handler_IamRole_C121BD5E.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "\${aws_s3_bucket.Code.bucket}", + "s3_key": "\${aws_s3_object.QueueRef_QueueArnField_Handler_S3Object_6AD15EC0.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [], + }, + }, + "QueueRef_QueueUrlField_Handler_42A4BE81": { + "architectures": [ + "arm64", + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "QUEUE_ARN_23984aca": "arn:aws:sqs:us-west-2:123456789012:MyQueue1234", + "WING_FUNCTION_NAME": "Handler-c8ec121e", + }, + }, + "function_name": "Handler-c8ec121e", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "\${aws_iam_role.QueueRef_QueueUrlField_Handler_IamRole_709A15CB.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "\${aws_s3_bucket.Code.bucket}", + "s3_key": "\${aws_s3_object.QueueRef_QueueUrlField_Handler_S3Object_225E3B78.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [], + }, + }, + }, + "aws_s3_bucket": { + "Code": { + "bucket_prefix": "code-c84a50b1-", + }, + }, + "aws_s3_object": { + "Function_S3Object_C62A0C2D": { + "bucket": "\${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "", + }, + "QueueRef_AwsConsoleField_Handler_S3Object_C4237D42": { + "bucket": "\${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "", + }, + "QueueRef_QueueArnField_Handler_S3Object_6AD15EC0": { + "bucket": "\${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "", + }, + "QueueRef_QueueUrlField_Handler_S3Object_225E3B78": { + "bucket": "\${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "", + }, + }, + }, +} +`; + +exports[`QueueRef in an TFAWS app can be used to reference an existing queue 2`] = ` +{ + "tree": { + "children": { + "root": { + "children": { + "Default": { + "children": { + "Code": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Code", + "path": "root/Default/Code", + }, + "Function": { + "children": { + "Asset": { + "constructInfo": { + "fqn": "cdktf.TerraformAsset", + "version": "0.20.3", + }, + "id": "Asset", + "path": "root/Default/Function/Asset", + }, + "CloudwatchLogGroup": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "CloudwatchLogGroup", + "path": "root/Default/Function/CloudwatchLogGroup", + }, + "Default": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Default", + "path": "root/Default/Function/Default", + }, + "IamRole": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRole", + "path": "root/Default/Function/IamRole", + }, + "IamRolePolicy": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicy", + "path": "root/Default/Function/IamRolePolicy", + }, + "IamRolePolicyAttachment": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicyAttachment", + "path": "root/Default/Function/IamRolePolicyAttachment", + }, + "S3Object": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "S3Object", + "path": "root/Default/Function/S3Object", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud function (FaaS)", + "title": "Function", + }, + "id": "Function", + "path": "root/Default/Function", + }, + "ParameterRegistrar": { + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "ParameterRegistrar", + "path": "root/Default/ParameterRegistrar", + }, + "QueueRef": { + "children": { + "AwsConsoleField": { + "children": { + "Handler": { + "children": { + "Asset": { + "constructInfo": { + "fqn": "cdktf.TerraformAsset", + "version": "0.20.3", + }, + "id": "Asset", + "path": "root/Default/QueueRef/AwsConsoleField/Handler/Asset", + }, + "CloudwatchLogGroup": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "CloudwatchLogGroup", + "path": "root/Default/QueueRef/AwsConsoleField/Handler/CloudwatchLogGroup", + }, + "Default": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Default", + "path": "root/Default/QueueRef/AwsConsoleField/Handler/Default", + }, + "IamRole": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRole", + "path": "root/Default/QueueRef/AwsConsoleField/Handler/IamRole", + }, + "IamRolePolicy": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicy", + "path": "root/Default/QueueRef/AwsConsoleField/Handler/IamRolePolicy", + }, + "IamRolePolicyAttachment": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicyAttachment", + "path": "root/Default/QueueRef/AwsConsoleField/Handler/IamRolePolicyAttachment", + }, + "S3Object": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "S3Object", + "path": "root/Default/QueueRef/AwsConsoleField/Handler/S3Object", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud function (FaaS)", + "title": "Function", + }, + "id": "Handler", + "path": "root/Default/QueueRef/AwsConsoleField/Handler", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "hidden": true, + }, + "id": "AwsConsoleField", + "path": "root/Default/QueueRef/AwsConsoleField", + }, + "QueueArnField": { + "children": { + "Handler": { + "children": { + "Asset": { + "constructInfo": { + "fqn": "cdktf.TerraformAsset", + "version": "0.20.3", + }, + "id": "Asset", + "path": "root/Default/QueueRef/QueueArnField/Handler/Asset", + }, + "CloudwatchLogGroup": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "CloudwatchLogGroup", + "path": "root/Default/QueueRef/QueueArnField/Handler/CloudwatchLogGroup", + }, + "Default": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Default", + "path": "root/Default/QueueRef/QueueArnField/Handler/Default", + }, + "IamRole": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRole", + "path": "root/Default/QueueRef/QueueArnField/Handler/IamRole", + }, + "IamRolePolicy": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicy", + "path": "root/Default/QueueRef/QueueArnField/Handler/IamRolePolicy", + }, + "IamRolePolicyAttachment": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicyAttachment", + "path": "root/Default/QueueRef/QueueArnField/Handler/IamRolePolicyAttachment", + }, + "S3Object": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "S3Object", + "path": "root/Default/QueueRef/QueueArnField/Handler/S3Object", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud function (FaaS)", + "title": "Function", + }, + "id": "Handler", + "path": "root/Default/QueueRef/QueueArnField/Handler", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "hidden": true, + }, + "id": "QueueArnField", + "path": "root/Default/QueueRef/QueueArnField", + }, + "QueueUrlField": { + "children": { + "Handler": { + "children": { + "Asset": { + "constructInfo": { + "fqn": "cdktf.TerraformAsset", + "version": "0.20.3", + }, + "id": "Asset", + "path": "root/Default/QueueRef/QueueUrlField/Handler/Asset", + }, + "CloudwatchLogGroup": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "CloudwatchLogGroup", + "path": "root/Default/QueueRef/QueueUrlField/Handler/CloudwatchLogGroup", + }, + "Default": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Default", + "path": "root/Default/QueueRef/QueueUrlField/Handler/Default", + }, + "IamRole": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRole", + "path": "root/Default/QueueRef/QueueUrlField/Handler/IamRole", + }, + "IamRolePolicy": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicy", + "path": "root/Default/QueueRef/QueueUrlField/Handler/IamRolePolicy", + }, + "IamRolePolicyAttachment": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicyAttachment", + "path": "root/Default/QueueRef/QueueUrlField/Handler/IamRolePolicyAttachment", + }, + "S3Object": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "S3Object", + "path": "root/Default/QueueRef/QueueUrlField/Handler/S3Object", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud function (FaaS)", + "title": "Function", + }, + "id": "Handler", + "path": "root/Default/QueueRef/QueueUrlField/Handler", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "hidden": true, + }, + "id": "QueueUrlField", + "path": "root/Default/QueueRef/QueueUrlField", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "color": "pink", + "ui": [ + { + "handler": "root/Default/QueueRef/QueueUrlField/Handler", + "kind": "field", + "label": "SQS Queue URL", + "link": true, + }, + { + "handler": "root/Default/QueueRef/AwsConsoleField/Handler", + "kind": "field", + "label": "AWS Console", + "link": true, + }, + { + "handler": "root/Default/QueueRef/QueueArnField/Handler", + "kind": "field", + "label": "SQS Queue ARN", + }, + ], + }, + "id": "QueueRef", + "path": "root/Default/QueueRef", + }, + "aws": { + "constructInfo": { + "fqn": "cdktf.TerraformProvider", + "version": "0.20.3", + }, + "id": "aws", + "path": "root/Default/aws", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "Default", + "path": "root/Default", + }, + "backend": { + "constructInfo": { + "fqn": "cdktf.LocalBackend", + "version": "0.20.3", + }, + "id": "backend", + "path": "root/backend", + }, + }, + "constructInfo": { + "fqn": "cdktf.TerraformStack", + "version": "0.20.3", + }, + "id": "root", + "path": "root", + }, + }, + "constructInfo": { + "fqn": "cdktf.App", + "version": "0.20.3", + }, + "id": "App", + "path": "", + }, + "version": "tree-0.1", +} +`; + exports[`default queue behavior 1`] = ` { "resource": { diff --git a/libs/wingsdk/test/target-tf-aws/queue.test.ts b/libs/wingsdk/test/target-tf-aws/queue.test.ts index 319f9ea1d40..20539d4a254 100644 --- a/libs/wingsdk/test/target-tf-aws/queue.test.ts +++ b/libs/wingsdk/test/target-tf-aws/queue.test.ts @@ -1,9 +1,11 @@ import * as cdktf from "cdktf"; import { test, expect } from "vitest"; -import { Queue } from "../../src/cloud"; +import { Function, IFunctionClient, Queue } from "../../src/cloud"; +import { QueueRef } from "../../src/shared-aws"; import { Testing } from "../../src/simulator"; import * as std from "../../src/std"; import * as tfaws from "../../src/target-tf-aws"; +import { SimApp } from "../sim-app"; import { mkdtemp, sanitizeCode, @@ -94,7 +96,7 @@ test("queue name valid", () => { cdktf.Testing.toHaveResourceWithProperties(output, "aws_sqs_queue", { name: `The-Incredible_Queue-01-${queue.node.addr.substring(0, 8)}`, }) - ); + ).toBeTruthy(); expect(tfSanitize(output)).toMatchSnapshot(); expect(treeJsonOf(app.outdir)).toMatchSnapshot(); }); @@ -110,7 +112,102 @@ test("replace invalid character from queue name", () => { cdktf.Testing.toHaveResourceWithProperties(output, "aws_sqs_queue", { name: `The-Incredible-Queue-${queue.node.addr.substring(0, 8)}`, }) + ).toBeTruthy(); + expect(tfSanitize(output)).toMatchSnapshot(); + expect(treeJsonOf(app.outdir)).toMatchSnapshot(); +}); + +test("QueueRef fails with an invalid ARN", async () => { + const app = new SimApp(); + expect(() => { + new QueueRef(app, "QueueRef", "not-an_arn"); + }).toThrow(/"not-an_arn" is not a valid Amazon SQS ARN/); +}); + +test("QueueRef in a SimApp can be used to reference an existing queue within a simulated app", async () => { + const app = new SimApp(); + + const queueRef = new QueueRef( + app, + "QueueRef", + "arn:aws:sqs:us-west-2:123456789012:MyQueue1234" ); + + // we can't really make a remote call here, so we'll just check that + // we have the right inflight client with the right queue name. + new Function( + app, + "Function", + Testing.makeHandler( + `async handle() { + if (!this.queue.client) { + throw new Error("Queue AWS SDK client not found"); + } + if (this.queue.client.constructor.name !== "SQSClient") { + throw new Error("Queue AWS SDK client is not an SQSClient"); + } + return this.queue._queueUrlOrArn; // yes, internal stuff + }`, + { + queue: { obj: queueRef, ops: ["push"] }, + } + ) + ); + + expect(queueRef.queueArn).toStrictEqual( + "arn:aws:sqs:us-west-2:123456789012:MyQueue1234" + ); + + const sim = await app.startSimulator(); + + const fn = sim.getResource("root/Function") as IFunctionClient; + + const reply = await fn.invoke(); + expect(reply).toStrictEqual("arn:aws:sqs:us-west-2:123456789012:MyQueue1234"); +}); + +test("QueueRef in an TFAWS app can be used to reference an existing queue", () => { + // GIVEN + const app = new tfaws.App({ outdir: mkdtemp(), entrypointDir: __dirname }); + const queue = new QueueRef( + app, + "QueueRef", + "arn:aws:sqs:us-west-2:123456789012:MyQueue1234" + ); + + const handler = Testing.makeHandler(``, { + queue: { obj: queue, ops: ["push", "approxSize"] }, + }); + + new Function(app, "Function", handler); + + const output = app.synth(); + + // THEN + + const statements = JSON.parse( + JSON.parse(output).resource.aws_iam_role_policy + .Function_IamRolePolicy_E3B26607.policy + ).Statement; + + expect(statements).toStrictEqual([ + { + Action: ["sqs:GetQueueUrl"], + Effect: "Allow", + Resource: ["arn:aws:sqs:us-west-2:123456789012:MyQueue1234"], + }, + { + Action: ["sqs:SendMessage"], + Effect: "Allow", + Resource: ["arn:aws:sqs:us-west-2:123456789012:MyQueue1234"], + }, + { + Action: ["sqs:GetQueueAttributes"], + Effect: "Allow", + Resource: ["arn:aws:sqs:us-west-2:123456789012:MyQueue1234"], + }, + ]); + expect(tfSanitize(output)).toMatchSnapshot(); expect(treeJsonOf(app.outdir)).toMatchSnapshot(); }); diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_compile_tf-aws.md new file mode 100644 index 00000000000..5559abc4cb1 --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_compile_tf-aws.md @@ -0,0 +1,501 @@ +# [queue-ref.test.w](../../../../../../examples/tests/sdk_tests/queue/queue-ref.test.w) | compile | tf-aws + +## main.tf.json +```json +{ + "//": { + "metadata": { + "backend": "local", + "stackName": "root", + "version": "0.20.3" + }, + "outputs": {} + }, + "provider": { + "aws": [ + {} + ] + }, + "resource": { + "aws_cloudwatch_log_group": { + "Function_CloudwatchLogGroup_ABDCF4C4": { + "//": { + "metadata": { + "path": "root/Default/Default/Function/CloudwatchLogGroup", + "uniqueId": "Function_CloudwatchLogGroup_ABDCF4C4" + } + }, + "name": "/aws/lambda/Function-c852aba6", + "retention_in_days": 30 + }, + "Queue-SetConsumer0_CloudwatchLogGroup_56C2891C": { + "//": { + "metadata": { + "path": "root/Default/Default/Queue-SetConsumer0/CloudwatchLogGroup", + "uniqueId": "Queue-SetConsumer0_CloudwatchLogGroup_56C2891C" + } + }, + "name": "/aws/lambda/Queue-SetConsumer0-c83c303c", + "retention_in_days": 30 + }, + "QueueRef_AwsConsoleField_Handler_CloudwatchLogGroup_1FAC3ED5": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/AwsConsoleField/Handler/CloudwatchLogGroup", + "uniqueId": "QueueRef_AwsConsoleField_Handler_CloudwatchLogGroup_1FAC3ED5" + } + }, + "name": "/aws/lambda/Handler-c85cfbef", + "retention_in_days": 30 + }, + "QueueRef_QueueArnField_Handler_CloudwatchLogGroup_BDFDF613": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueArnField/Handler/CloudwatchLogGroup", + "uniqueId": "QueueRef_QueueArnField_Handler_CloudwatchLogGroup_BDFDF613" + } + }, + "name": "/aws/lambda/Handler-c8c3c90a", + "retention_in_days": 30 + }, + "QueueRef_QueueUrlField_Handler_CloudwatchLogGroup_8AB6E3DD": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueUrlField/Handler/CloudwatchLogGroup", + "uniqueId": "QueueRef_QueueUrlField_Handler_CloudwatchLogGroup_8AB6E3DD" + } + }, + "name": "/aws/lambda/Handler-c8ec121e", + "retention_in_days": 30 + } + }, + "aws_dynamodb_table": { + "Counter": { + "//": { + "metadata": { + "path": "root/Default/Default/Counter/Default", + "uniqueId": "Counter" + } + }, + "attribute": [ + { + "name": "id", + "type": "S" + } + ], + "billing_mode": "PAY_PER_REQUEST", + "hash_key": "id", + "name": "wing-counter-Counter-c824ef62" + } + }, + "aws_iam_role": { + "Function_IamRole_678BE84C": { + "//": { + "metadata": { + "path": "root/Default/Default/Function/IamRole", + "uniqueId": "Function_IamRole_678BE84C" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + }, + "Queue-SetConsumer0_IamRole_7F9ED9ED": { + "//": { + "metadata": { + "path": "root/Default/Default/Queue-SetConsumer0/IamRole", + "uniqueId": "Queue-SetConsumer0_IamRole_7F9ED9ED" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + }, + "QueueRef_AwsConsoleField_Handler_IamRole_0DD0004C": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/AwsConsoleField/Handler/IamRole", + "uniqueId": "QueueRef_AwsConsoleField_Handler_IamRole_0DD0004C" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + }, + "QueueRef_QueueArnField_Handler_IamRole_C121BD5E": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueArnField/Handler/IamRole", + "uniqueId": "QueueRef_QueueArnField_Handler_IamRole_C121BD5E" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + }, + "QueueRef_QueueUrlField_Handler_IamRole_709A15CB": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueUrlField/Handler/IamRole", + "uniqueId": "QueueRef_QueueUrlField_Handler_IamRole_709A15CB" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + } + }, + "aws_iam_role_policy": { + "Function_IamRolePolicy_E3B26607": { + "//": { + "metadata": { + "path": "root/Default/Default/Function/IamRolePolicy", + "uniqueId": "Function_IamRolePolicy_E3B26607" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:GetQueueUrl\"],\"Resource\":[\"${aws_sqs_queue.Queue.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"sqs:SendMessage\"],\"Resource\":[\"${aws_sqs_queue.Queue.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"sqs:ReceiveMessage\",\"sqs:DeleteMessage\"],\"Resource\":[\"${aws_sqs_queue.Queue.arn}\"],\"Effect\":\"Allow\"}]}", + "role": "${aws_iam_role.Function_IamRole_678BE84C.name}" + }, + "Queue-SetConsumer0_IamRolePolicy_0299B5AB": { + "//": { + "metadata": { + "path": "root/Default/Default/Queue-SetConsumer0/IamRolePolicy", + "uniqueId": "Queue-SetConsumer0_IamRolePolicy_0299B5AB" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:ReceiveMessage\",\"sqs:ChangeMessageVisibility\",\"sqs:GetQueueUrl\",\"sqs:DeleteMessage\",\"sqs:GetQueueAttributes\"],\"Resource\":[\"${aws_sqs_queue.Queue.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"dynamodb:UpdateItem\"],\"Resource\":[\"${aws_dynamodb_table.Counter.arn}\"],\"Effect\":\"Allow\"}]}", + "role": "${aws_iam_role.Queue-SetConsumer0_IamRole_7F9ED9ED.name}" + }, + "QueueRef_AwsConsoleField_Handler_IamRolePolicy_184A5238": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/AwsConsoleField/Handler/IamRolePolicy", + "uniqueId": "QueueRef_AwsConsoleField_Handler_IamRolePolicy_184A5238" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:GetQueueUrl\"],\"Resource\":[\"${aws_sqs_queue.Queue.arn}\"],\"Effect\":\"Allow\"}]}", + "role": "${aws_iam_role.QueueRef_AwsConsoleField_Handler_IamRole_0DD0004C.name}" + }, + "QueueRef_QueueArnField_Handler_IamRolePolicy_64EE3F4B": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueArnField/Handler/IamRolePolicy", + "uniqueId": "QueueRef_QueueArnField_Handler_IamRolePolicy_64EE3F4B" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"none:null\",\"Resource\":\"*\"}]}", + "role": "${aws_iam_role.QueueRef_QueueArnField_Handler_IamRole_C121BD5E.name}" + }, + "QueueRef_QueueUrlField_Handler_IamRolePolicy_A9F2F773": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueUrlField/Handler/IamRolePolicy", + "uniqueId": "QueueRef_QueueUrlField_Handler_IamRolePolicy_A9F2F773" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:GetQueueUrl\"],\"Resource\":[\"${aws_sqs_queue.Queue.arn}\"],\"Effect\":\"Allow\"}]}", + "role": "${aws_iam_role.QueueRef_QueueUrlField_Handler_IamRole_709A15CB.name}" + } + }, + "aws_iam_role_policy_attachment": { + "Function_IamRolePolicyAttachment_CACE1358": { + "//": { + "metadata": { + "path": "root/Default/Default/Function/IamRolePolicyAttachment", + "uniqueId": "Function_IamRolePolicyAttachment_CACE1358" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.Function_IamRole_678BE84C.name}" + }, + "Queue-SetConsumer0_IamRolePolicyAttachment_4A4C5C5D": { + "//": { + "metadata": { + "path": "root/Default/Default/Queue-SetConsumer0/IamRolePolicyAttachment", + "uniqueId": "Queue-SetConsumer0_IamRolePolicyAttachment_4A4C5C5D" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.Queue-SetConsumer0_IamRole_7F9ED9ED.name}" + }, + "QueueRef_AwsConsoleField_Handler_IamRolePolicyAttachment_1F892467": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/AwsConsoleField/Handler/IamRolePolicyAttachment", + "uniqueId": "QueueRef_AwsConsoleField_Handler_IamRolePolicyAttachment_1F892467" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.QueueRef_AwsConsoleField_Handler_IamRole_0DD0004C.name}" + }, + "QueueRef_QueueArnField_Handler_IamRolePolicyAttachment_BA9083B2": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueArnField/Handler/IamRolePolicyAttachment", + "uniqueId": "QueueRef_QueueArnField_Handler_IamRolePolicyAttachment_BA9083B2" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.QueueRef_QueueArnField_Handler_IamRole_C121BD5E.name}" + }, + "QueueRef_QueueUrlField_Handler_IamRolePolicyAttachment_9FA1E52D": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueUrlField/Handler/IamRolePolicyAttachment", + "uniqueId": "QueueRef_QueueUrlField_Handler_IamRolePolicyAttachment_9FA1E52D" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.QueueRef_QueueUrlField_Handler_IamRole_709A15CB.name}" + } + }, + "aws_lambda_event_source_mapping": { + "Queue_EventSourceMapping_8332F7DC": { + "//": { + "metadata": { + "path": "root/Default/Default/Queue/EventSourceMapping", + "uniqueId": "Queue_EventSourceMapping_8332F7DC" + } + }, + "batch_size": 1, + "event_source_arn": "${aws_sqs_queue.Queue.arn}", + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + } + }, + "aws_lambda_function": { + "Function": { + "//": { + "metadata": { + "path": "root/Default/Default/Function/Default", + "uniqueId": "Function" + } + }, + "architectures": [ + "arm64" + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "QUEUE_ARN_23984aca": "${aws_sqs_queue.Queue.arn}", + "WING_FUNCTION_NAME": "Function-c852aba6", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "Function-c852aba6", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "${aws_iam_role.Function_IamRole_678BE84C.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.Function_S3Object_C62A0C2D.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + }, + "Queue-SetConsumer0": { + "//": { + "metadata": { + "path": "root/Default/Default/Queue-SetConsumer0/Default", + "uniqueId": "Queue-SetConsumer0" + } + }, + "architectures": [ + "arm64" + ], + "environment": { + "variables": { + "DYNAMODB_TABLE_NAME_6cb5a3a4": "${aws_dynamodb_table.Counter.name}", + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "Queue-SetConsumer0-c83c303c", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "Queue-SetConsumer0-c83c303c", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "${aws_iam_role.Queue-SetConsumer0_IamRole_7F9ED9ED.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.Queue-SetConsumer0_S3Object_2AD0A795.key}", + "timeout": "${aws_sqs_queue.Queue.visibility_timeout_seconds}", + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + }, + "QueueRef_AwsConsoleField_Handler_2D13D4BF": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/AwsConsoleField/Handler/Default", + "uniqueId": "QueueRef_AwsConsoleField_Handler_2D13D4BF" + } + }, + "architectures": [ + "arm64" + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "QUEUE_ARN_23984aca": "${aws_sqs_queue.Queue.arn}", + "WING_FUNCTION_NAME": "Handler-c85cfbef", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "Handler-c85cfbef", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "${aws_iam_role.QueueRef_AwsConsoleField_Handler_IamRole_0DD0004C.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.QueueRef_AwsConsoleField_Handler_S3Object_C4237D42.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + }, + "QueueRef_QueueArnField_Handler_0B477E06": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueArnField/Handler/Default", + "uniqueId": "QueueRef_QueueArnField_Handler_0B477E06" + } + }, + "architectures": [ + "arm64" + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "Handler-c8c3c90a", + "WING_TARGET": "tf-aws", + "WING_TOKEN_TFTOKEN_TOKEN_20": "${jsonencode(aws_sqs_queue.Queue.arn)}" + } + }, + "function_name": "Handler-c8c3c90a", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "${aws_iam_role.QueueRef_QueueArnField_Handler_IamRole_C121BD5E.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.QueueRef_QueueArnField_Handler_S3Object_6AD15EC0.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + }, + "QueueRef_QueueUrlField_Handler_42A4BE81": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueUrlField/Handler/Default", + "uniqueId": "QueueRef_QueueUrlField_Handler_42A4BE81" + } + }, + "architectures": [ + "arm64" + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "QUEUE_ARN_23984aca": "${aws_sqs_queue.Queue.arn}", + "WING_FUNCTION_NAME": "Handler-c8ec121e", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "Handler-c8ec121e", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "${aws_iam_role.QueueRef_QueueUrlField_Handler_IamRole_709A15CB.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.QueueRef_QueueUrlField_Handler_S3Object_225E3B78.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + } + }, + "aws_s3_bucket": { + "Code": { + "//": { + "metadata": { + "path": "root/Default/Code", + "uniqueId": "Code" + } + }, + "bucket_prefix": "code-c84a50b1-" + } + }, + "aws_s3_object": { + "Function_S3Object_C62A0C2D": { + "//": { + "metadata": { + "path": "root/Default/Default/Function/S3Object", + "uniqueId": "Function_S3Object_C62A0C2D" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + }, + "Queue-SetConsumer0_S3Object_2AD0A795": { + "//": { + "metadata": { + "path": "root/Default/Default/Queue-SetConsumer0/S3Object", + "uniqueId": "Queue-SetConsumer0_S3Object_2AD0A795" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + }, + "QueueRef_AwsConsoleField_Handler_S3Object_C4237D42": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/AwsConsoleField/Handler/S3Object", + "uniqueId": "QueueRef_AwsConsoleField_Handler_S3Object_C4237D42" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + }, + "QueueRef_QueueArnField_Handler_S3Object_6AD15EC0": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueArnField/Handler/S3Object", + "uniqueId": "QueueRef_QueueArnField_Handler_S3Object_6AD15EC0" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + }, + "QueueRef_QueueUrlField_Handler_S3Object_225E3B78": { + "//": { + "metadata": { + "path": "root/Default/Default/QueueRef/QueueUrlField/Handler/S3Object", + "uniqueId": "QueueRef_QueueUrlField_Handler_S3Object_225E3B78" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + } + }, + "aws_sqs_queue": { + "Queue": { + "//": { + "metadata": { + "path": "root/Default/Default/Queue/Default", + "uniqueId": "Queue" + } + }, + "message_retention_seconds": 3600, + "name": "Queue-c822c726", + "visibility_timeout_seconds": 30 + } + } + } +} +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_test_sim.md new file mode 100644 index 00000000000..069a0ab792f --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_test_sim.md @@ -0,0 +1,13 @@ +# [queue-ref.test.w](../../../../../../examples/tests/sdk_tests/queue/queue-ref.test.w) | test | sim + +## stdout.log +```log +pass ─ queue-ref.test.wsim » root/env0/test:queueArn returns the arn +pass ─ queue-ref.test.wsim » root/env1/test:push() sends a request to aws, fails because we are using a dummy queue + + +Tests 2 passed (2) +Test Files 1 passed (1) +Duration +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md index f234c9defb4..91693506e99 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md @@ -347,7 +347,7 @@ module.exports = function({ }) { "uniqueId": "BigPublisher_b2_oncreate-OnMessage0_IamRolePolicy_983EC08F" } }, - "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:SendMessage\"],\"Resource\":[\"${aws_sqs_queue.BigPublisher_Queue_2C024F97.arn}\"],\"Effect\":\"Allow\"}]}", + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:GetQueueUrl\"],\"Resource\":[\"${aws_sqs_queue.BigPublisher_Queue_2C024F97.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"sqs:SendMessage\"],\"Resource\":[\"${aws_sqs_queue.BigPublisher_Queue_2C024F97.arn}\"],\"Effect\":\"Allow\"}]}", "role": "${aws_iam_role.BigPublisher_b2_oncreate-OnMessage0_IamRole_FF154497.name}" } },