1import * as AWS from "@/AWS";
2import { Rule } from "@/AWS/Rbin";
3import * as Test from "@/Test/Alchemy";
4import * as rbin from "@distilled.cloud/aws/rbin";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11/** Typed wait-until-gone: getRule must settle on RULE_NOT_FOUND. */
12const assertRuleGone = (identifier: string) =>
13 rbin.getRule({ Identifier: identifier }).pipe(
14 Effect.flatMap(() =>
15 Effect.fail(new Error(`retention rule ${identifier} still exists`)),
16 ),
17 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
18 Effect.retry({
19 while: (e) => e instanceof Error,
20 schedule: Schedule.max([
21 Schedule.fixed("2 seconds"),
22 Schedule.recurs(10),
23 ]),
24 }),
25 );
26
27describe(
28 "AWS.Rbin.Rule",
29 { tags: ["provider:aws", "provider:aws:rbin", "live"] },
30 () => {
31 test.provider(
32 "lifecycle: create, update in place, replace on resourceType, destroy",
33 (stack) =>
34 Effect.gen(function* () {
35 yield* stack.destroy();
36
37 // Create a tag-level rule (scoped by resourceTags so it never
38 // retains other test suites' deleted snapshots).
39 const created = yield* stack.deploy(
40 Effect.gen(function* () {
41 const rule = yield* Rule("SnapshotRetention", {
42 resourceType: "EBS_SNAPSHOT",
43 retentionPeriod: "7 days",
44 description: "alchemy rbin lifecycle test",
45 resourceTags: [
46 { key: "alchemy-rbin-test", value: "snapshots" },
47 ],
48 tags: { purpose: "alchemy-test" },
49 });
50 return {
51 identifier: rule.identifier,
52 ruleArn: rule.ruleArn,
53 };
54 }),
55 );
56
57 // Out-of-band verification via distilled.
58 const observed = yield* rbin.getRule({
59 Identifier: created.identifier,
60 });
61 expect(observed.RuleArn).toEqual(created.ruleArn);
62 expect(observed.ResourceType).toEqual("EBS_SNAPSHOT");
63 expect(observed.RetentionPeriod).toEqual({
64 RetentionPeriodValue: 7,
65 RetentionPeriodUnit: "DAYS",
66 });
67 expect(observed.Description).toEqual("alchemy rbin lifecycle test");
68 expect(observed.ResourceTags).toEqual([
69 {
70 ResourceTagKey: "alchemy-rbin-test",
71 ResourceTagValue: "snapshots",
72 },
73 ]);
74 expect(observed.Status).toEqual("available");
75
76 const tags = yield* rbin.listTagsForResource({
77 ResourceArn: created.ruleArn,
78 });
79 expect(tags.Tags).toEqual(
80 expect.arrayContaining([
81 { Key: "purpose", Value: "alchemy-test" },
82 { Key: "alchemy::id", Value: "SnapshotRetention" },
83 ]),
84 );
85
86 // Update in place: retention period, description, resource tags,
87 // and rule tags (remove `purpose`, add `owner`).
88 const updated = yield* stack.deploy(
89 Effect.gen(function* () {
90 const rule = yield* Rule("SnapshotRetention", {
91 resourceType: "EBS_SNAPSHOT",
92 retentionPeriod: "14 days",
93 description: "alchemy rbin lifecycle test (updated)",
94 resourceTags: [{ key: "alchemy-rbin-test", value: "updated" }],
95 tags: { owner: "alchemy" },
96 });
97 return { identifier: rule.identifier, ruleArn: rule.ruleArn };
98 }),
99 );
100 expect(updated.identifier).toEqual(created.identifier);
101
102 const observedUpdated = yield* rbin.getRule({
103 Identifier: updated.identifier,
104 });
105 expect(observedUpdated.RetentionPeriod?.RetentionPeriodValue).toEqual(
106 14,
107 );
108 expect(observedUpdated.Description).toEqual(
109 "alchemy rbin lifecycle test (updated)",
110 );
111 expect(observedUpdated.ResourceTags).toEqual([
112 {
113 ResourceTagKey: "alchemy-rbin-test",
114 ResourceTagValue: "updated",
115 },
116 ]);
117
118 const updatedTags = yield* rbin.listTagsForResource({
119 ResourceArn: updated.ruleArn,
120 });
121 expect(updatedTags.Tags).toEqual(
122 expect.arrayContaining([{ Key: "owner", Value: "alchemy" }]),
123 );
124 expect(updatedTags.Tags).not.toEqual(
125 expect.arrayContaining([{ Key: "purpose", Value: "alchemy-test" }]),
126 );
127
128 // Replacement: changing the resource type replaces the rule.
129 const replaced = yield* stack.deploy(
130 Effect.gen(function* () {
131 const rule = yield* Rule("SnapshotRetention", {
132 resourceType: "EC2_IMAGE",
133 retentionPeriod: "14 days",
134 description: "alchemy rbin lifecycle test (updated)",
135 resourceTags: [{ key: "alchemy-rbin-test", value: "updated" }],
136 tags: { owner: "alchemy" },
137 });
138 return { identifier: rule.identifier, ruleArn: rule.ruleArn };
139 }),
140 );
141 expect(replaced.identifier).not.toEqual(created.identifier);
142
143 const observedReplaced = yield* rbin.getRule({
144 Identifier: replaced.identifier,
145 });
146 expect(observedReplaced.ResourceType).toEqual("EC2_IMAGE");
147 // The replaced (old) rule is deleted after the new one lands.
148 yield* assertRuleGone(created.identifier);
149
150 yield* stack.destroy();
151 yield* assertRuleGone(replaced.identifier);
152 }),
153 { timeout: 180_000 },
154 );
155 },
156);