| 1 | import * as AWS from "@/AWS"; |
| 2 | import { Application } from "@/AWS/CodeDeploy/Application.ts"; |
| 3 | import { DeploymentConfig } from "@/AWS/CodeDeploy/DeploymentConfig.ts"; |
| 4 | import { DeploymentGroup } from "@/AWS/CodeDeploy/DeploymentGroup.ts"; |
| 5 | import * as Provider from "@/Provider"; |
| 6 | import * as Test from "@/Test/Alchemy"; |
| 7 | import * as codedeploy from "@distilled.cloud/aws/codedeploy"; |
| 8 | import { expect } from "alchemy-test"; |
| 9 | import * as Effect from "effect/Effect"; |
| 10 | |
| 11 | const { test } = Test.make({ providers: AWS.providers() }); |
| 12 | |
| 13 | const applicationName = "alchemy-test-cd-app"; |
| 14 | const deploymentGroupName = "alchemy-test-cd-dg"; |
| 15 | const deploymentConfigName = "alchemy-test-cd-config"; |
| 16 | |
| 17 | const getGroup = codedeploy |
| 18 | .getDeploymentGroup({ applicationName, deploymentGroupName }) |
| 19 | .pipe( |
| 20 | Effect.map((res) => res.deploymentGroupInfo), |
| 21 | Effect.catchTag( |
| 22 | [ |
| 23 | "DeploymentGroupDoesNotExistException", |
| 24 | "ApplicationDoesNotExistException", |
| 25 | ], |
| 26 | () => Effect.succeed(undefined), |
| 27 | ), |
| 28 | ); |
| 29 | |
| 30 | // A Lambda-platform deployment group: an application, a custom canary |
| 31 | // deployment configuration, a CodeDeploy service role (assumed by |
| 32 | // codedeploy.amazonaws.com, granted the AWS-managed Lambda policy), and the |
| 33 | // group tying a deployment config to the application. |
| 34 | const makeStack = (useCustomConfig: boolean) => |
| 35 | Effect.gen(function* () { |
| 36 | const app = yield* Application("App", { |
| 37 | applicationName, |
| 38 | computePlatform: "Lambda", |
| 39 | }); |
| 40 | const config = yield* DeploymentConfig("Config", { |
| 41 | deploymentConfigName, |
| 42 | computePlatform: "Lambda", |
| 43 | trafficRoutingConfig: { |
| 44 | type: "TimeBasedCanary", |
| 45 | timeBasedCanary: { canaryPercentage: 10, canaryInterval: 1 }, |
| 46 | }, |
| 47 | }); |
| 48 | const role = yield* AWS.IAM.Role("DeployRole", { |
| 49 | assumeRolePolicyDocument: { |
| 50 | Version: "2012-10-17", |
| 51 | Statement: [ |
| 52 | { |
| 53 | Effect: "Allow", |
| 54 | Principal: { Service: "codedeploy.amazonaws.com" }, |
| 55 | Action: ["sts:AssumeRole"], |
| 56 | }, |
| 57 | ], |
| 58 | }, |
| 59 | managedPolicyArns: [ |
| 60 | "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda", |
| 61 | ], |
| 62 | }); |
| 63 | const group = yield* DeploymentGroup("Group", { |
| 64 | applicationName: app.applicationName, |
| 65 | deploymentGroupName, |
| 66 | serviceRoleArn: role.roleArn, |
| 67 | deploymentConfigName: useCustomConfig |
| 68 | ? config.deploymentConfigName |
| 69 | : "CodeDeployDefault.LambdaAllAtOnce", |
| 70 | deploymentStyle: { |
| 71 | deploymentType: "BLUE_GREEN", |
| 72 | deploymentOption: "WITH_TRAFFIC_CONTROL", |
| 73 | }, |
| 74 | autoRollbackConfiguration: { |
| 75 | enabled: true, |
| 76 | events: ["DEPLOYMENT_FAILURE"], |
| 77 | }, |
| 78 | tags: { env: "test" }, |
| 79 | }); |
| 80 | return { app, config, group }; |
| 81 | }); |
| 82 | |
| 83 | test.provider( |
| 84 | "lifecycle: create Lambda application + deployment group, update config, destroy", |
| 85 | (stack) => |
| 86 | Effect.gen(function* () { |
| 87 | yield* stack.destroy(); |
| 88 | |
| 89 | // Create. |
| 90 | const deployed = yield* stack.deploy(makeStack(false)); |
| 91 | expect(deployed.app.applicationName).toBe(applicationName); |
| 92 | expect(deployed.app.applicationArn).toContain(":application:"); |
| 93 | expect(deployed.app.computePlatform).toBe("Lambda"); |
| 94 | expect(deployed.group.deploymentGroupName).toBe(deploymentGroupName); |
| 95 | expect(deployed.group.deploymentGroupArn).toContain(":deploymentgroup:"); |
| 96 | expect(deployed.config.deploymentConfigName).toBe(deploymentConfigName); |
| 97 | expect(deployed.config.deploymentConfigArn).toContain( |
| 98 | ":deploymentconfig:", |
| 99 | ); |
| 100 | expect(deployed.config.deploymentConfigId).toBeTruthy(); |
| 101 | expect(deployed.config.computePlatform).toBe("Lambda"); |
| 102 | |
| 103 | // Out-of-band verification via distilled. |
| 104 | const created = yield* getGroup; |
| 105 | expect(created?.deploymentGroupName).toBe(deploymentGroupName); |
| 106 | expect(created?.computePlatform).toBe("Lambda"); |
| 107 | expect(created?.deploymentConfigName).toBe( |
| 108 | "CodeDeployDefault.LambdaAllAtOnce", |
| 109 | ); |
| 110 | |
| 111 | const appCheck = yield* codedeploy.getApplication({ applicationName }); |
| 112 | expect(appCheck.application?.computePlatform).toBe("Lambda"); |
| 113 | |
| 114 | // Out-of-band verification of the custom deployment configuration. |
| 115 | const configCheck = yield* codedeploy.getDeploymentConfig({ |
| 116 | deploymentConfigName, |
| 117 | }); |
| 118 | expect(configCheck.deploymentConfigInfo?.computePlatform).toBe("Lambda"); |
| 119 | expect( |
| 120 | configCheck.deploymentConfigInfo?.trafficRoutingConfig?.timeBasedCanary |
| 121 | ?.canaryPercentage, |
| 122 | ).toBe(10); |
| 123 | |
| 124 | // Canonical list() coverage for Application. |
| 125 | const appProvider = yield* Provider.findProvider(Application); |
| 126 | const apps = yield* appProvider.list(); |
| 127 | expect(apps.some((a) => a.applicationName === applicationName)).toBe( |
| 128 | true, |
| 129 | ); |
| 130 | |
| 131 | // Canonical list() coverage for DeploymentConfig — custom configs are |
| 132 | // listed, AWS-managed CodeDeployDefault.* ones are filtered out. |
| 133 | const configProvider = yield* Provider.findProvider(DeploymentConfig); |
| 134 | const configs = yield* configProvider.list(); |
| 135 | expect( |
| 136 | configs.some((c) => c.deploymentConfigName === deploymentConfigName), |
| 137 | ).toBe(true); |
| 138 | expect( |
| 139 | configs.every( |
| 140 | (c) => !c.deploymentConfigName.startsWith("CodeDeployDefault."), |
| 141 | ), |
| 142 | ).toBe(true); |
| 143 | |
| 144 | // Update — point the group at the custom config in place (no |
| 145 | // replacement). |
| 146 | yield* stack.deploy(makeStack(true)); |
| 147 | const updated = yield* getGroup; |
| 148 | expect(updated?.deploymentConfigName).toBe(deploymentConfigName); |
| 149 | |
| 150 | // Destroy — verify the group, application, and config are gone |
| 151 | // out-of-band. |
| 152 | yield* stack.destroy(); |
| 153 | const afterGroup = yield* getGroup; |
| 154 | expect(afterGroup).toBeUndefined(); |
| 155 | const afterApp = yield* codedeploy |
| 156 | .getApplication({ applicationName }) |
| 157 | .pipe( |
| 158 | Effect.map((res) => res.application), |
| 159 | Effect.catchTag("ApplicationDoesNotExistException", () => |
| 160 | Effect.succeed(undefined), |
| 161 | ), |
| 162 | ); |
| 163 | expect(afterApp).toBeUndefined(); |
| 164 | const afterConfig = yield* codedeploy |
| 165 | .getDeploymentConfig({ deploymentConfigName }) |
| 166 | .pipe( |
| 167 | Effect.map((res) => res.deploymentConfigInfo), |
| 168 | Effect.catchTag("DeploymentConfigDoesNotExistException", () => |
| 169 | Effect.succeed(undefined), |
| 170 | ), |
| 171 | ); |
| 172 | expect(afterConfig).toBeUndefined(); |
| 173 | }), |
| 174 | { |
| 175 | tags: [ |
| 176 | "provider:aws", |
| 177 | "provider:aws:codedeploy", |
| 178 | "provider:aws:iam", |
| 179 | "live", |
| 180 | ], |
| 181 | timeout: 300_000, |
| 182 | }, |
| 183 | ); |