diff --git a/README.md b/README.md index 3d9f2fd4..378af267 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,50 @@ You can propagate your custom tags from your existing service using `propagate-t propagate-tags: SERVICE ``` +### EBS Volume Configuration +This action supports configuring Amazon EBS volumes for both services and standalone tasks. + +For Services (Update Service): + +```yaml + - name: Deploy to Amazon ECS with EBS Volume + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: task-definition.json + service: my-service + cluster: my-cluster + wait-for-service-stability: true + service-managed-ebs-volume-name: "ebs1" + service-managed-ebs-volume: '{"sizeInGiB": 30, "volumeType": "gp3", "encrypted": true, "roleArn":"arn:aws:iam:::role/ebs-role"}' +``` + +Note: Your task definition must include a volume that is configuredAtLaunch: + +```json + ... + "volumes": [ + { + "name": "ebs1", + "configuredAtLaunch": true + } + ], + ... +``` + +For Standalone Tasks (RunTask): + +```yaml + - name: Deploy to Amazon ECS + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: task-definition.json + cluster: my-cluster + run-task: true + run-task-launch-type: EC2 + run-task-managed-ebs-volume-name: "ebs1" + run-task-managed-ebs-volume: '{"filesystemType":"xfs", "roleArn":"arn:aws:iam:::role/github-actions-setup-stack-EBSRole-YwVmgS4g7gQE", "encrypted":false, "sizeInGiB":30}' +``` + ## Credentials and Region This action relies on the [default behavior of the AWS SDK for Javascript](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html) to determine AWS credentials and region. diff --git a/action.yml b/action.yml index 08b3098c..c71365bf 100644 --- a/action.yml +++ b/action.yml @@ -40,6 +40,12 @@ inputs: force-new-deployment: description: 'Whether to force a new deployment of the service. Valid value is "true". Will default to not force a new deployment.' required: false + service-managed-ebs-volume-name: + description: "The name of the volume, to be manage in the ECS service. This value must match the volume name from the Volume object in the task definition, that was configuredAtLaunch." + required: false + service-managed-ebs-volume: + description: "A JSON object defining the configuration settings for the EBS Service volume that was ConfiguredAtLaunch. You can configure size, volumeType, IOPS, throughput, snapshot and encryption in ServiceManagedEBSVolumeConfiguration. Currently, the only supported volume type is an Amazon EBS volume." + required: false run-task: description: 'A boolean indicating whether to run a stand-alone task in a ECS cluster. Task will run before the service is updated if both are provided. Default value is false .' required: false @@ -67,6 +73,12 @@ inputs: run-task-tags: description: 'A JSON array of tags.' required: false + run-task-managed-ebs-volume-name: + description: "The name of the volume. This value must match the volume name from the Volume object in the task definition, that was configuredAtLaunch." + required: false + run-task-managed-ebs-volume: + description: "A JSON object defining the configuration settings for the Amazon EBS task volume that was configuredAtLaunch. These settings are used to create each Amazon EBS volume, with one volume created for each task in the service. The Amazon EBS volumes are visible in your account in the Amazon EC2 console once they are created." + required: false wait-for-task-stopped: description: 'Whether to wait for the task to stop when running it outside of a service. Will default to not wait.' required: false diff --git a/dist/index.js b/dist/index.js index c051664e..d486c142 100644 --- a/dist/index.js +++ b/dist/index.js @@ -39,6 +39,8 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa const assignPublicIP = core.getInput('run-task-assign-public-IP', { required: false }) || 'DISABLED'; const tags = JSON.parse(core.getInput('run-task-tags', { required: false }) || '[]'); const capacityProviderStrategy = JSON.parse(core.getInput('run-task-capacity-provider-strategy', { required: false }) || '[]'); + const runTaskManagedEBSVolumeName = core.getInput('run-task-managed-ebs-volume-name', { required: false }) || ''; + const runTaskManagedEBSVolume = core.getInput('run-task-managed-ebs-volume', { required: false }) || '{}'; let awsvpcConfiguration = {} @@ -53,6 +55,20 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa if(assignPublicIP != "" && (subnetIds != "" || securityGroupIds != "")){ awsvpcConfiguration["assignPublicIp"] = assignPublicIP } + let volumeConfigurations = []; + let taskManagedEBSVolumeObject; + + if (runTaskManagedEBSVolumeName != '') { + if (runTaskManagedEBSVolume != '{}') { + taskManagedEBSVolumeObject = convertToManagedEbsVolumeObject(runTaskManagedEBSVolume); + volumeConfigurations = [{ + name: runTaskManagedEBSVolumeName, + managedEBSVolume: taskManagedEBSVolumeObject + }]; + } else { + core.warning(`run-task-managed-ebs-volume-name provided without run-task-managed-ebs-volume value. VolumeConfigurations property will not be included in the RunTask API call`); + } + } const runTaskResponse = await ecs.runTask({ startedBy: startedBy, @@ -65,7 +81,8 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa launchType: capacityProviderStrategy.length === 0 ? launchType : null, networkConfiguration: Object.keys(awsvpcConfiguration).length === 0 ? null : { awsvpcConfiguration: awsvpcConfiguration }, enableECSManagedTags: enableECSManagedTags, - tags: tags + tags: tags, + volumeConfigurations: volumeConfigurations }); core.debug(`Run task response ${JSON.stringify(runTaskResponse)}`) @@ -92,6 +109,47 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa } } +function convertToManagedEbsVolumeObject(managedEbsVolume) { + managedEbsVolumeObject = {} + const ebsVolumeObject = JSON.parse(managedEbsVolume); + if ('roleArn' in ebsVolumeObject){ // required property + managedEbsVolumeObject.roleArn = ebsVolumeObject.roleArn; + core.debug(`Found RoleArn ${ebsVolumeObject['roleArn']}`); + } else { + throw new Error('managed-ebs-volume must provide "role-arn" to associate with the EBS volume') + } + + if ('encrypted' in ebsVolumeObject) { + managedEbsVolumeObject.encrypted = ebsVolumeObject.encrypted; + } + if ('filesystemType' in ebsVolumeObject) { + managedEbsVolumeObject.filesystemType = ebsVolumeObject.filesystemType; + } + if ('iops' in ebsVolumeObject) { + managedEbsVolumeObject.iops = ebsVolumeObject.iops; + } + if ('kmsKeyId' in ebsVolumeObject) { + managedEbsVolumeObject.kmsKeyId = ebsVolumeObject.kmsKeyId; + } + if ('sizeInGiB' in ebsVolumeObject) { + managedEbsVolumeObject.sizeInGiB = ebsVolumeObject.sizeInGiB; + } + if ('snapshotId' in ebsVolumeObject) { + managedEbsVolumeObject.snapshotId = ebsVolumeObject.snapshotId; + } + if ('tagSpecifications' in ebsVolumeObject) { + managedEbsVolumeObject.tagSpecifications = ebsVolumeObject.tagSpecifications; + } + if (('throughput' in ebsVolumeObject) && (('volumeType' in ebsVolumeObject) && (ebsVolumeObject.volumeType == 'gp3'))){ + managedEbsVolumeObject.throughput = ebsVolumeObject.throughput; + } + if ('volumeType' in ebsVolumeObject) { + managedEbsVolumeObject.volumeType = ebsVolumeObject.volumeType; + } + core.debug(`Created managedEbsVolumeObject: ${JSON.stringify(managedEbsVolumeObject)}`); + return managedEbsVolumeObject; +} + // Poll tasks until they enter a stopped state async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) { if (waitForMinutes > MAX_WAIT_MINUTES) { @@ -142,13 +200,32 @@ async function tasksExitCode(ecs, clusterName, taskArns) { async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) { core.debug('Updating the service'); + const serviceManagedEBSVolumeName = core.getInput('service-managed-ebs-volume-name', { required: false }) || ''; + const serviceManagedEBSVolume = core.getInput('service-managed-ebs-volume', { required: false }) || '{}'; + + let volumeConfigurations = []; + let serviceManagedEbsVolumeObject; + + if (serviceManagedEBSVolumeName != '') { + if (serviceManagedEBSVolume != '{}') { + serviceManagedEbsVolumeObject = convertToManagedEbsVolumeObject(serviceManagedEBSVolume); + volumeConfigurations = [{ + name: serviceManagedEBSVolumeName, + managedEBSVolume: serviceManagedEbsVolumeObject + }]; + } else { + core.warning('service-managed-ebs-volume-name provided without service-managed-ebs-volume value. VolumeConfigurations property will not be included in the UpdateService API call'); + } + } + let params = { cluster: clusterName, service: service, taskDefinition: taskDefArn, forceNewDeployment: forceNewDeployment, enableECSManagedTags: enableECSManagedTags, - propagateTags: propagateTags + propagateTags: propagateTags, + volumeConfigurations: volumeConfigurations }; // Add the desiredCount property only if it is defined and a number. diff --git a/index.js b/index.js index b24760e2..e8e65b04 100644 --- a/index.js +++ b/index.js @@ -33,6 +33,8 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa const assignPublicIP = core.getInput('run-task-assign-public-IP', { required: false }) || 'DISABLED'; const tags = JSON.parse(core.getInput('run-task-tags', { required: false }) || '[]'); const capacityProviderStrategy = JSON.parse(core.getInput('run-task-capacity-provider-strategy', { required: false }) || '[]'); + const runTaskManagedEBSVolumeName = core.getInput('run-task-managed-ebs-volume-name', { required: false }) || ''; + const runTaskManagedEBSVolume = core.getInput('run-task-managed-ebs-volume', { required: false }) || '{}'; let awsvpcConfiguration = {} @@ -47,6 +49,20 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa if(assignPublicIP != "" && (subnetIds != "" || securityGroupIds != "")){ awsvpcConfiguration["assignPublicIp"] = assignPublicIP } + let volumeConfigurations = []; + let taskManagedEBSVolumeObject; + + if (runTaskManagedEBSVolumeName != '') { + if (runTaskManagedEBSVolume != '{}') { + taskManagedEBSVolumeObject = convertToManagedEbsVolumeObject(runTaskManagedEBSVolume); + volumeConfigurations = [{ + name: runTaskManagedEBSVolumeName, + managedEBSVolume: taskManagedEBSVolumeObject + }]; + } else { + core.warning(`run-task-managed-ebs-volume-name provided without run-task-managed-ebs-volume value. VolumeConfigurations property will not be included in the RunTask API call`); + } + } const runTaskResponse = await ecs.runTask({ startedBy: startedBy, @@ -59,7 +75,8 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa launchType: capacityProviderStrategy.length === 0 ? launchType : null, networkConfiguration: Object.keys(awsvpcConfiguration).length === 0 ? null : { awsvpcConfiguration: awsvpcConfiguration }, enableECSManagedTags: enableECSManagedTags, - tags: tags + tags: tags, + volumeConfigurations: volumeConfigurations }); core.debug(`Run task response ${JSON.stringify(runTaskResponse)}`) @@ -86,6 +103,47 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa } } +function convertToManagedEbsVolumeObject(managedEbsVolume) { + managedEbsVolumeObject = {} + const ebsVolumeObject = JSON.parse(managedEbsVolume); + if ('roleArn' in ebsVolumeObject){ // required property + managedEbsVolumeObject.roleArn = ebsVolumeObject.roleArn; + core.debug(`Found RoleArn ${ebsVolumeObject['roleArn']}`); + } else { + throw new Error('managed-ebs-volume must provide "role-arn" to associate with the EBS volume') + } + + if ('encrypted' in ebsVolumeObject) { + managedEbsVolumeObject.encrypted = ebsVolumeObject.encrypted; + } + if ('filesystemType' in ebsVolumeObject) { + managedEbsVolumeObject.filesystemType = ebsVolumeObject.filesystemType; + } + if ('iops' in ebsVolumeObject) { + managedEbsVolumeObject.iops = ebsVolumeObject.iops; + } + if ('kmsKeyId' in ebsVolumeObject) { + managedEbsVolumeObject.kmsKeyId = ebsVolumeObject.kmsKeyId; + } + if ('sizeInGiB' in ebsVolumeObject) { + managedEbsVolumeObject.sizeInGiB = ebsVolumeObject.sizeInGiB; + } + if ('snapshotId' in ebsVolumeObject) { + managedEbsVolumeObject.snapshotId = ebsVolumeObject.snapshotId; + } + if ('tagSpecifications' in ebsVolumeObject) { + managedEbsVolumeObject.tagSpecifications = ebsVolumeObject.tagSpecifications; + } + if (('throughput' in ebsVolumeObject) && (('volumeType' in ebsVolumeObject) && (ebsVolumeObject.volumeType == 'gp3'))){ + managedEbsVolumeObject.throughput = ebsVolumeObject.throughput; + } + if ('volumeType' in ebsVolumeObject) { + managedEbsVolumeObject.volumeType = ebsVolumeObject.volumeType; + } + core.debug(`Created managedEbsVolumeObject: ${JSON.stringify(managedEbsVolumeObject)}`); + return managedEbsVolumeObject; +} + // Poll tasks until they enter a stopped state async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) { if (waitForMinutes > MAX_WAIT_MINUTES) { @@ -136,13 +194,32 @@ async function tasksExitCode(ecs, clusterName, taskArns) { async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) { core.debug('Updating the service'); + const serviceManagedEBSVolumeName = core.getInput('service-managed-ebs-volume-name', { required: false }) || ''; + const serviceManagedEBSVolume = core.getInput('service-managed-ebs-volume', { required: false }) || '{}'; + + let volumeConfigurations = []; + let serviceManagedEbsVolumeObject; + + if (serviceManagedEBSVolumeName != '') { + if (serviceManagedEBSVolume != '{}') { + serviceManagedEbsVolumeObject = convertToManagedEbsVolumeObject(serviceManagedEBSVolume); + volumeConfigurations = [{ + name: serviceManagedEBSVolumeName, + managedEBSVolume: serviceManagedEbsVolumeObject + }]; + } else { + core.warning('service-managed-ebs-volume-name provided without service-managed-ebs-volume value. VolumeConfigurations property will not be included in the UpdateService API call'); + } + } + let params = { cluster: clusterName, service: service, taskDefinition: taskDefArn, forceNewDeployment: forceNewDeployment, enableECSManagedTags: enableECSManagedTags, - propagateTags: propagateTags + propagateTags: propagateTags, + volumeConfigurations: volumeConfigurations }; // Add the desiredCount property only if it is defined and a number. diff --git a/index.test.js b/index.test.js index 493c520c..1b3f1889 100644 --- a/index.test.js +++ b/index.test.js @@ -187,7 +187,8 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', forceNewDeployment: false, enableECSManagedTags: null, - propagateTags: 'NONE' + propagateTags: 'NONE', + volumeConfigurations: [] }); expect(waitUntilServicesStable).toHaveBeenCalledTimes(0); expect(core.info).toBeCalledWith("Deployment started. Watch this deployment's progress in the Amazon ECS console: https://fake-region.console.aws.amazon.com/ecs/v2/clusters/cluster-789/services/service-456/events?region=fake-region"); @@ -220,7 +221,8 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', forceNewDeployment: false, enableECSManagedTags: null, - propagateTags: 'NONE' + propagateTags: 'NONE', + volumeConfigurations: [] }); expect(waitUntilServicesStable).toHaveBeenCalledTimes(0); expect(core.info).toBeCalledWith("Deployment started. Watch this deployment's progress in the Amazon ECS console: https://fake-region.console.aws.amazon.com/ecs/v2/clusters/cluster-789/services/service-456/events?region=fake-region"); @@ -955,7 +957,8 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', forceNewDeployment: false, enableECSManagedTags: null, - propagateTags: 'NONE' + propagateTags: 'NONE', + volumeConfigurations: [] }); expect(waitUntilServicesStable).toHaveBeenNthCalledWith( 1, @@ -996,7 +999,8 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', forceNewDeployment: false, enableECSManagedTags: null, - propagateTags: 'NONE' + propagateTags: 'NONE', + volumeConfigurations: [] }); expect(waitUntilServicesStable).toHaveBeenNthCalledWith( 1, @@ -1037,7 +1041,8 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', forceNewDeployment: false, enableECSManagedTags: null, - propagateTags: 'NONE' + propagateTags: 'NONE', + volumeConfigurations: [] }); expect(waitUntilServicesStable).toHaveBeenNthCalledWith( 1, @@ -1080,7 +1085,8 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', forceNewDeployment: true, enableECSManagedTags: null, - propagateTags: 'NONE' + propagateTags: 'NONE', + volumeConfigurations: [] }); }); @@ -1106,7 +1112,8 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', forceNewDeployment: false, enableECSManagedTags: null, - propagateTags: 'NONE' + propagateTags: 'NONE', + volumeConfigurations: [] }); }); @@ -1153,7 +1160,8 @@ describe('Deploy to ECS', () => { overrides: {"containerOverrides": []}, networkConfiguration: null, enableECSManagedTags: null, - tags: [] + tags: [], + volumeConfigurations: [] }); expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); @@ -1195,7 +1203,8 @@ describe('Deploy to ECS', () => { overrides: { containerOverrides: [{ name: 'someapp', command: 'somecmd' }] }, networkConfiguration: { awsvpcConfiguration: { subnets: ['a', 'b'], securityGroups: ['c', 'd'], assignPublicIp: "DISABLED" } }, enableECSManagedTags: false, - tags: [{"key": "project", "value": "myproject"}] + tags: [{"key": "project", "value": "myproject"}], + volumeConfigurations: [] }); expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); }); @@ -1238,6 +1247,7 @@ describe('Deploy to ECS', () => { networkConfiguration: { awsvpcConfiguration: { subnets: ['a', 'b'], securityGroups: ['c', 'd'], assignPublicIp: "DISABLED" } }, enableECSManagedTags: false, tags: [{"key": "project", "value": "myproject"}], + volumeConfigurations: [] }); expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); }); @@ -1278,6 +1288,7 @@ describe('Deploy to ECS', () => { forceNewDeployment: false, enableECSManagedTags: null, propagateTags: 'NONE', + volumeConfigurations: [] }); expect(mockRunTask).toHaveBeenCalledWith({ startedBy: 'someJoe', @@ -1288,7 +1299,8 @@ describe('Deploy to ECS', () => { overrides: { containerOverrides: [{ name: 'someapp', command: 'somecmd' }] }, networkConfiguration: { awsvpcConfiguration: { subnets: ['a', 'b'], securityGroups: ['c', 'd'], assignPublicIp: "DISABLED" } }, enableECSManagedTags: null, - tags: [] + tags: [], + volumeConfigurations: [] }); expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); }); @@ -1349,7 +1361,8 @@ describe('Deploy to ECS', () => { overrides: { containerOverrides: [] }, networkConfiguration: null, enableECSManagedTags: null, - tags: [] + tags: [], + volumeConfigurations: [] }); }); @@ -1377,7 +1390,8 @@ describe('Deploy to ECS', () => { overrides: { containerOverrides: [] }, networkConfiguration: null, enableECSManagedTags: true, - tags: [] + tags: [], + volumeConfigurations: [] }); }); @@ -1405,7 +1419,8 @@ describe('Deploy to ECS', () => { overrides: { containerOverrides: [] }, networkConfiguration: null, enableECSManagedTags: false, - tags: [] + tags: [], + volumeConfigurations: [] }); }); @@ -1603,7 +1618,8 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', forceNewDeployment: false, enableECSManagedTags: null, - propagateTags: 'SERVICE' + propagateTags: 'SERVICE', + volumeConfigurations: [] }); }); @@ -1635,7 +1651,8 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', forceNewDeployment: false, enableECSManagedTags: true, - propagateTags: 'SERVICE' + propagateTags: 'SERVICE', + volumeConfigurations: [] }); }); @@ -1667,7 +1684,235 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', forceNewDeployment: false, enableECSManagedTags: false, - propagateTags: 'SERVICE' + propagateTags: 'SERVICE', + volumeConfigurations: [] + }); + }); + + test('update service with new EBS volume configuration', async () => { + core.getInput = jest + .fn() + .mockImplementation((name) => { + console.log(`Getting input for: ${name}`); + const inputs = { + 'task-definition': 'task-definition.json', + 'service': 'service-456', + 'cluster': 'cluster-789', + 'service-managed-ebs-volume-name': 'ebs1', + 'service-managed-ebs-volume': JSON.stringify({ + filesystemType: "xfs", + roleArn: "arn:aws:iam::123:role/ebs-role", + encrypted: false, + sizeInGiB: 30 + }), + 'run-task': 'false' + }; + return inputs[name] || ''; + }); + + await run(); + + expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { + cluster: 'cluster-789', + service: 'service-456', + taskDefinition: 'task:def:arn', + forceNewDeployment: false, + enableECSManagedTags: null, + propagateTags: 'NONE', + volumeConfigurations: [{ + name: 'ebs1', + managedEBSVolume: { + filesystemType: "xfs", + roleArn: "arn:aws:iam::123:role/ebs-role", + encrypted: false, + sizeInGiB: 30 + } + }] + }); + }); + + test('update existing EBS volume configuration in an ECS Service', async () => { + // First create a service with initial EBS configuration + core.getInput = jest + .fn() + .mockImplementation((name) => { + const inputs = { + 'task-definition': 'task-definition.json', + 'service': 'service-456', + 'cluster': 'cluster-789', + 'service-managed-ebs-volume-name': 'ebs1', + 'service-managed-ebs-volume': JSON.stringify({ + filesystemType: "xfs", + roleArn: "arn:aws:iam::123:role/ebs-role", + encrypted: false, + sizeInGiB: 30 + }), + 'run-task': 'false' + }; + return inputs[name] || ''; + }); + + await run(); + + // Then update the service with new EBS configuration + core.getInput = jest + .fn() + .mockImplementation((name) => { + const inputs = { + 'task-definition': 'task-definition.json', + 'service': 'service-456', + 'cluster': 'cluster-789', + 'service-managed-ebs-volume-name': 'ebs1', + 'service-managed-ebs-volume': JSON.stringify({ + filesystemType: "xfs", + roleArn: "arn:aws:iam::123:role/ebs-role", + encrypted: true, // Changed + sizeInGiB: 50 // Changed + }), + 'run-task': 'false' + }; + return inputs[name] || ''; + }); + + await run(); + + // Verify the second call had the updated configuration + expect(mockEcsUpdateService).toHaveBeenNthCalledWith(2, { + cluster: 'cluster-789', + service: 'service-456', + taskDefinition: 'task:def:arn', + forceNewDeployment: false, + enableECSManagedTags: null, + propagateTags: 'NONE', + volumeConfigurations: [{ + name: 'ebs1', + managedEBSVolume: { + filesystemType: "xfs", + roleArn: "arn:aws:iam::123:role/ebs-role", + encrypted: true, + sizeInGiB: 50 + } + }] + }); + }); + + test('remove service EBS volume configuration', async () => { + + // First call - create service with EBS configuration + core.getInput = jest + .fn() + .mockImplementation((name) => { + const inputs = { + 'task-definition': 'task-definition.json', + 'service': 'service-456', + 'cluster': 'cluster-789', + 'service-managed-ebs-volume-name': 'ebs1', + 'service-managed-ebs-volume': JSON.stringify({ + filesystemType: "xfs", + roleArn: "arn:aws:iam::123:role/ebs-role", + encrypted: false, + sizeInGiB: 30 + }), + 'run-task': 'false' + }; + return inputs[name] || ''; + }); + + await run(); + + // Second call - remove EBS configuration + core.getInput = jest + .fn() + .mockImplementation((name) => { + const inputs = { + 'task-definition': 'task-definition.json', + 'service': 'service-456', + 'cluster': 'cluster-789', + 'run-task': 'false' + }; + return inputs[name] || ''; + }); + + await run(); + + // Verify both calls were made correctly + expect(mockEcsUpdateService).toHaveBeenCalledTimes(2); + + // Verify first call had the EBS configuration + expect(mockEcsUpdateService.mock.calls[0][0]).toEqual({ + cluster: 'cluster-789', + service: 'service-456', + taskDefinition: 'task:def:arn', + forceNewDeployment: false, + enableECSManagedTags: null, + propagateTags: 'NONE', + volumeConfigurations: [{ + name: 'ebs1', + managedEBSVolume: { + filesystemType: "xfs", + roleArn: "arn:aws:iam::123:role/ebs-role", + encrypted: false, + sizeInGiB: 30 + } + }] + }); + + // Verify second call had empty volume configurations + expect(mockEcsUpdateService.mock.calls[1][0]).toEqual({ + cluster: 'cluster-789', + service: 'service-456', + taskDefinition: 'task:def:arn', + forceNewDeployment: false, + enableECSManagedTags: null, + propagateTags: 'NONE', + volumeConfigurations: [] + }); + }); + + test('run task with EBS volume configuration', async () => { + core.getInput = jest + .fn() + .mockImplementation((name) => { + const inputs = { + 'task-definition': 'task-definition.json', + 'service': '', + 'cluster': 'cluster-789', + 'run-task': 'true', + 'run-task-launch-type': 'EC2', + 'run-task-managed-ebs-volume-name': 'ebs1', + 'run-task-managed-ebs-volume': JSON.stringify({ + filesystemType: "xfs", + roleArn: "arn:aws:iam::123:role/ebs-role", + encrypted: false, + sizeInGiB: 30 + }) + }; + return inputs[name] || ''; + }); + + await run(); + + expect(mockRunTask).toHaveBeenCalledWith({ + cluster: 'cluster-789', + taskDefinition: 'task:def:arn', + startedBy: 'GitHub-Actions', + capacityProviderStrategy: null, + launchType: 'EC2', + enableECSManagedTags: null, + tags: [], + overrides: { + containerOverrides: [] + }, + networkConfiguration: null, + volumeConfigurations: [{ + name: 'ebs1', + managedEBSVolume: { + filesystemType: "xfs", + roleArn: "arn:aws:iam::123:role/ebs-role", + encrypted: false, + sizeInGiB: 30 + } + }] }); }); -}); +}); \ No newline at end of file