@@ -52,6 +52,14 @@ import {
5252 type ApplicationProtocol ,
5353 type LogDriver ,
5454 type EFSVolumeConfiguration ,
55+ type EFSAuthorizationConfig ,
56+ type DockerVolumeConfiguration ,
57+ type FSxWindowsFileServerVolumeConfiguration ,
58+ type FSxWindowsFileServerAuthorizationConfig ,
59+ type HostVolumeProperties ,
60+ type Scope ,
61+ type EFSTransitEncryption ,
62+ type EFSAuthorizationConfigIAM ,
5563 type AssignPublicIp ,
5664 type ContainerCondition ,
5765 type EnvironmentFileType ,
@@ -1177,15 +1185,33 @@ export class ECSProvider implements ResourceProvider {
11771185 }
11781186
11791187 /**
1180- * Convert CFn Volumes to ECS SDK format
1188+ * Convert CFn Volumes to ECS SDK format.
1189+ *
1190+ * Every nested volume-configuration block is PascalCase in the CFn
1191+ * template and camelCase in the ECS SDK input, so each sub-block runs
1192+ * through a dedicated converter — the same PascalCase->camelCase trap
1193+ * already fixed for the ContainerDefinitions sub-arrays
1194+ * (convertEnvironment / convertSecrets / convertMountPoints etc.).
1195+ * Before issue #815, `Host` / `EFSVolumeConfiguration` were cast through
1196+ * raw (so their nested keys reached the SDK still PascalCase) and
1197+ * `DockerVolumeConfiguration` / `FSxWindowsFileServerVolumeConfiguration`
1198+ * were not mapped at all (silently dropped).
11811199 */
11821200 private convertVolumes ( volumes ?: Array < Record < string , unknown > > ) : Volume [ ] | undefined {
11831201 if ( ! volumes ) return undefined ;
11841202
11851203 return volumes . map ( ( v ) => ( {
11861204 name : v [ 'Name' ] as string ,
1187- host : v [ 'Host' ] as { sourcePath ?: string } | undefined ,
1188- efsVolumeConfiguration : v [ 'EFSVolumeConfiguration' ] as EFSVolumeConfiguration | undefined ,
1205+ host : this . convertVolumeHost ( v [ 'Host' ] as Record < string , unknown > | undefined ) ,
1206+ dockerVolumeConfiguration : this . convertDockerVolumeConfiguration (
1207+ v [ 'DockerVolumeConfiguration' ] as Record < string , unknown > | undefined
1208+ ) ,
1209+ efsVolumeConfiguration : this . convertEFSVolumeConfiguration (
1210+ v [ 'EFSVolumeConfiguration' ] as Record < string , unknown > | undefined
1211+ ) ,
1212+ fsxWindowsFileServerVolumeConfiguration : this . convertFSxWindowsVolumeConfiguration (
1213+ v [ 'FSxWindowsFileServerVolumeConfiguration' ] as Record < string , unknown > | undefined
1214+ ) ,
11891215 // ConfiguredAtLaunch marks the volume as attach-at-launch so a
11901216 // same-stack AWS::ECS::Service can carry a matching
11911217 // VolumeConfigurations entry (managed EBS volume). Dropping it made
@@ -1196,6 +1222,181 @@ export class ECSProvider implements ResourceProvider {
11961222 } ) ) ;
11971223 }
11981224
1225+ /**
1226+ * Convert CFn Volumes[].Host to ECS SDK format.
1227+ * CFn: `{SourcePath}` -> SDK: `{sourcePath}`.
1228+ */
1229+ private convertVolumeHost ( host ?: Record < string , unknown > ) : HostVolumeProperties | undefined {
1230+ if ( ! host ) return undefined ;
1231+ return {
1232+ sourcePath : host [ 'SourcePath' ] as string | undefined ,
1233+ } ;
1234+ }
1235+
1236+ /**
1237+ * Convert CFn Volumes[].DockerVolumeConfiguration to ECS SDK format.
1238+ * CFn: `{Scope, Autoprovision, Driver, DriverOpts, Labels}`
1239+ * -> SDK: `{scope, autoprovision, driver, driverOpts, labels}`.
1240+ */
1241+ private convertDockerVolumeConfiguration (
1242+ config ?: Record < string , unknown >
1243+ ) : DockerVolumeConfiguration | undefined {
1244+ if ( ! config ) return undefined ;
1245+ return {
1246+ scope : config [ 'Scope' ] as Scope | undefined ,
1247+ autoprovision : this . coerceBool ( config [ 'Autoprovision' ] ) ,
1248+ driver : config [ 'Driver' ] as string | undefined ,
1249+ driverOpts : config [ 'DriverOpts' ] as Record < string , string > | undefined ,
1250+ labels : config [ 'Labels' ] as Record < string , string > | undefined ,
1251+ } ;
1252+ }
1253+
1254+ /**
1255+ * Convert CFn Volumes[].EFSVolumeConfiguration to ECS SDK format.
1256+ * CFn: `{FilesystemId, RootDirectory, TransitEncryption,
1257+ * TransitEncryptionPort, AuthorizationConfig}`
1258+ * -> SDK: `{fileSystemId, rootDirectory, transitEncryption,
1259+ * transitEncryptionPort, authorizationConfig}`.
1260+ * Note the CFn property is `FilesystemId` (lowercase `s`) while the SDK
1261+ * field is `fileSystemId` — they are not a simple first-letter case flip.
1262+ */
1263+ private convertEFSVolumeConfiguration (
1264+ config ?: Record < string , unknown >
1265+ ) : EFSVolumeConfiguration | undefined {
1266+ if ( ! config ) return undefined ;
1267+ return {
1268+ fileSystemId : config [ 'FilesystemId' ] as string ,
1269+ rootDirectory : config [ 'RootDirectory' ] as string | undefined ,
1270+ transitEncryption : config [ 'TransitEncryption' ] as EFSTransitEncryption | undefined ,
1271+ transitEncryptionPort :
1272+ config [ 'TransitEncryptionPort' ] !== undefined
1273+ ? Number ( config [ 'TransitEncryptionPort' ] )
1274+ : undefined ,
1275+ authorizationConfig : this . convertEFSAuthorizationConfig (
1276+ config [ 'AuthorizationConfig' ] as Record < string , unknown > | undefined
1277+ ) ,
1278+ } ;
1279+ }
1280+
1281+ /**
1282+ * Convert CFn EFSVolumeConfiguration.AuthorizationConfig to ECS SDK format.
1283+ * CFn: `{AccessPointId, IAM}` -> SDK: `{accessPointId, iam}`.
1284+ * Note the CFn key is `IAM` (all caps), NOT `Iam` — not a simple
1285+ * first-letter case flip (verified against the CDK L1 `IAM` mapping).
1286+ */
1287+ private convertEFSAuthorizationConfig (
1288+ config ?: Record < string , unknown >
1289+ ) : EFSAuthorizationConfig | undefined {
1290+ if ( ! config ) return undefined ;
1291+ return {
1292+ accessPointId : config [ 'AccessPointId' ] as string | undefined ,
1293+ iam : config [ 'IAM' ] as EFSAuthorizationConfigIAM | undefined ,
1294+ } ;
1295+ }
1296+
1297+ /**
1298+ * Convert CFn Volumes[].FSxWindowsFileServerVolumeConfiguration to ECS
1299+ * SDK format.
1300+ * CFn: `{FileSystemId, RootDirectory, AuthorizationConfig}`
1301+ * -> SDK: `{fileSystemId, rootDirectory, authorizationConfig}`.
1302+ */
1303+ private convertFSxWindowsVolumeConfiguration (
1304+ config ?: Record < string , unknown >
1305+ ) : FSxWindowsFileServerVolumeConfiguration | undefined {
1306+ if ( ! config ) return undefined ;
1307+ return {
1308+ fileSystemId : config [ 'FileSystemId' ] as string ,
1309+ rootDirectory : config [ 'RootDirectory' ] as string ,
1310+ authorizationConfig : this . convertFSxWindowsAuthorizationConfig (
1311+ config [ 'AuthorizationConfig' ] as Record < string , unknown > | undefined
1312+ ) as FSxWindowsFileServerAuthorizationConfig ,
1313+ } ;
1314+ }
1315+
1316+ /**
1317+ * Convert CFn FSxWindowsFileServerVolumeConfiguration.AuthorizationConfig
1318+ * to ECS SDK format.
1319+ * CFn: `{CredentialsParameter, Domain}`
1320+ * -> SDK: `{credentialsParameter, domain}`.
1321+ */
1322+ private convertFSxWindowsAuthorizationConfig (
1323+ config ?: Record < string , unknown >
1324+ ) : FSxWindowsFileServerAuthorizationConfig | undefined {
1325+ if ( ! config ) return undefined ;
1326+ return {
1327+ credentialsParameter : config [ 'CredentialsParameter' ] as string ,
1328+ domain : config [ 'Domain' ] as string ,
1329+ } ;
1330+ }
1331+
1332+ /**
1333+ * Convert the camelCase SDK `volumes` shape returned by
1334+ * DescribeTaskDefinition back to the PascalCase CFn template form, so the
1335+ * `readCurrentState` snapshot matches the deploy-time template
1336+ * representation for drift comparison (issue #815). Only volume keys
1337+ * present on the SDK side are emitted, so a future field cdkd does not
1338+ * map cannot surface as phantom drift. TaskDefinitions are immutable
1339+ * replace-only today, so this is forward-looking normalization.
1340+ */
1341+ private volumesToCfn ( volumes ?: Volume [ ] ) : Array < Record < string , unknown > > {
1342+ if ( ! volumes ) return [ ] ;
1343+ return volumes . map ( ( v ) => {
1344+ const out : Record < string , unknown > = { } ;
1345+ if ( v . name !== undefined ) out [ 'Name' ] = v . name ;
1346+ if ( v . host !== undefined ) {
1347+ const host : Record < string , unknown > = { } ;
1348+ if ( v . host . sourcePath !== undefined ) host [ 'SourcePath' ] = v . host . sourcePath ;
1349+ out [ 'Host' ] = host ;
1350+ }
1351+ if ( v . dockerVolumeConfiguration !== undefined ) {
1352+ const d = v . dockerVolumeConfiguration ;
1353+ const docker : Record < string , unknown > = { } ;
1354+ if ( d . scope !== undefined ) docker [ 'Scope' ] = d . scope ;
1355+ if ( d . autoprovision !== undefined ) docker [ 'Autoprovision' ] = d . autoprovision ;
1356+ if ( d . driver !== undefined ) docker [ 'Driver' ] = d . driver ;
1357+ if ( d . driverOpts !== undefined ) docker [ 'DriverOpts' ] = d . driverOpts ;
1358+ if ( d . labels !== undefined ) docker [ 'Labels' ] = d . labels ;
1359+ out [ 'DockerVolumeConfiguration' ] = docker ;
1360+ }
1361+ if ( v . efsVolumeConfiguration !== undefined ) {
1362+ const e = v . efsVolumeConfiguration ;
1363+ const efs : Record < string , unknown > = { } ;
1364+ if ( e . fileSystemId !== undefined ) efs [ 'FilesystemId' ] = e . fileSystemId ;
1365+ if ( e . rootDirectory !== undefined ) efs [ 'RootDirectory' ] = e . rootDirectory ;
1366+ if ( e . transitEncryption !== undefined ) efs [ 'TransitEncryption' ] = e . transitEncryption ;
1367+ if ( e . transitEncryptionPort !== undefined ) {
1368+ efs [ 'TransitEncryptionPort' ] = e . transitEncryptionPort ;
1369+ }
1370+ if ( e . authorizationConfig !== undefined ) {
1371+ const a = e . authorizationConfig ;
1372+ const auth : Record < string , unknown > = { } ;
1373+ if ( a . accessPointId !== undefined ) auth [ 'AccessPointId' ] = a . accessPointId ;
1374+ if ( a . iam !== undefined ) auth [ 'IAM' ] = a . iam ;
1375+ efs [ 'AuthorizationConfig' ] = auth ;
1376+ }
1377+ out [ 'EFSVolumeConfiguration' ] = efs ;
1378+ }
1379+ if ( v . fsxWindowsFileServerVolumeConfiguration !== undefined ) {
1380+ const f = v . fsxWindowsFileServerVolumeConfiguration ;
1381+ const fsx : Record < string , unknown > = { } ;
1382+ if ( f . fileSystemId !== undefined ) fsx [ 'FileSystemId' ] = f . fileSystemId ;
1383+ if ( f . rootDirectory !== undefined ) fsx [ 'RootDirectory' ] = f . rootDirectory ;
1384+ if ( f . authorizationConfig !== undefined ) {
1385+ const a = f . authorizationConfig ;
1386+ const auth : Record < string , unknown > = { } ;
1387+ if ( a . credentialsParameter !== undefined ) {
1388+ auth [ 'CredentialsParameter' ] = a . credentialsParameter ;
1389+ }
1390+ if ( a . domain !== undefined ) auth [ 'Domain' ] = a . domain ;
1391+ fsx [ 'AuthorizationConfig' ] = auth ;
1392+ }
1393+ out [ 'FSxWindowsFileServerVolumeConfiguration' ] = fsx ;
1394+ }
1395+ if ( v . configuredAtLaunch !== undefined ) out [ 'ConfiguredAtLaunch' ] = v . configuredAtLaunch ;
1396+ return out ;
1397+ } ) ;
1398+ }
1399+
11991400 /**
12001401 * Coerce a CFn boolean property to a real boolean at the wire boundary.
12011402 * CFn templates can carry booleans as the strings "true" / "false"
@@ -1528,7 +1729,7 @@ export class ECSProvider implements ResourceProvider {
15281729 : [ ] ;
15291730 if ( td . executionRoleArn !== undefined ) result [ 'ExecutionRoleArn' ] = td . executionRoleArn ;
15301731 if ( td . taskRoleArn !== undefined ) result [ 'TaskRoleArn' ] = td . taskRoleArn ;
1531- result [ 'Volumes' ] = td . volumes ?? [ ] ;
1732+ result [ 'Volumes' ] = this . volumesToCfn ( td . volumes ) ;
15321733 result [ 'PlacementConstraints' ] = td . placementConstraints ?? [ ] ;
15331734 if ( td . runtimePlatform ) result [ 'RuntimePlatform' ] = td . runtimePlatform ;
15341735 if ( td . proxyConfiguration ) result [ 'ProxyConfiguration' ] = td . proxyConfiguration ;
0 commit comments