1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as ElastiCache from "@distilled.cloud/aws/elasticache";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import {
7 assertReplicationGroupGone,
8 getProvisionedNetwork,
9 shareProvisionedNetwork,
10} from "./ProvisionedFixture.ts";
11
12const { test, beforeAll, afterAll } = Test.make({
13 providers: AWS.providers(),
14});
15shareProvisionedNetwork({ beforeAll, afterAll });
16test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
17 "updates a Valkey replication group to a newer supported engine version",
18 (stack) =>
19 Effect.gen(function* () {
20 const versions =
21 (yield* ElastiCache.describeCacheEngineVersions({
22 Engine: "valkey",
23 })).CacheEngineVersions?.map((version) => version.EngineVersion).filter(
24 (version): version is string => version !== undefined,
25 ) ?? [];
26 const ordered = [...new Set(versions)].sort((left, right) =>
27 left.localeCompare(right, undefined, { numeric: true }),
28 );
29 const sameMajorUpgrade = ordered
30 .map((initialVersion, index) => [initialVersion, ordered[index + 1]])
31 .find(
32 ([initialVersion, updatedVersion]) =>
33 updatedVersion !== undefined &&
34 initialVersion.split(".")[0] === updatedVersion.split(".")[0],
35 );
36 const [initialVersion, updatedVersion] = sameMajorUpgrade ?? [
37 ordered[0],
38 ordered[1],
39 ];
40 if (
41 !initialVersion ||
42 !updatedVersion ||
43 initialVersion === updatedVersion
44 ) {
45 return yield* Effect.fail(
46 new Error(
47 "AWS did not advertise two distinct Valkey engine versions",
48 ),
49 );
50 }
51
52 yield* stack.destroy();
53 const net = yield* getProvisionedNetwork;
54 const program = (engineVersion: string) =>
55 Effect.gen(function* () {
56 const cache = yield* AWS.ElastiCache.ReplicationGroup("Cache", {
57 description: "alchemy engine upgrade cache",
58 engine: "valkey",
59 engineVersion,
60 nodeType: "cache.t4g.micro",
61 subnetGroupName: net.subnetGroupName,
62 securityGroupIds: [net.securityGroupId],
63 replicasPerNodeGroup: 0,
64 transitEncryptionEnabled: true,
65 });
66 return { cache };
67 });
68
69 const created = yield* stack.deploy(program(initialVersion));
70 const updated = yield* stack.deploy(program(updatedVersion));
71 expect(updated.cache.replicationGroupId).toBe(
72 created.cache.replicationGroupId,
73 );
74 const group = yield* ElastiCache.describeReplicationGroups({
75 ReplicationGroupId: updated.cache.replicationGroupId,
76 });
77 const memberId = group.ReplicationGroups?.[0]?.MemberClusters?.[0];
78 if (!memberId)
79 return yield* Effect.fail(
80 new Error("replication group has no member cluster"),
81 );
82 const member = yield* ElastiCache.describeCacheClusters({
83 CacheClusterId: memberId,
84 });
85 // The API accepts a major/minor target such as `9.1`, but reports the
86 // fully resolved patch release (for example `9.1.0`).
87 expect(
88 member.CacheClusters?.[0]?.EngineVersion?.startsWith(updatedVersion),
89 ).toBe(true);
90
91 yield* stack.destroy();
92 yield* assertReplicationGroupGone(updated.cache.replicationGroupId);
93 }),
94 {
95 tags: [
96 "provider:aws",
97 "provider:aws:ec2",
98 "provider:aws:elasticache",
99 "live",
100 ],
101 timeout: 2_700_000,
102 },
103);