├── .gitignore ├── LICENSE ├── README.md ├── Setup.hs ├── examples └── tf-example.hs ├── scripts └── generate.hs ├── src └── Language │ └── Terraform │ ├── Aws.hs │ ├── Core.hs │ └── Util │ └── Text.hs ├── stack.yaml └── terraform-hs.cabal /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work 2 | *swp 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Tim Docker (c) 2017 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Tim Docker nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-hs 2 | 3 | A haskell EDSL for generating [terraform][] infrastructure specifications. 4 | 5 | The terraform system has some nice properties (an excellent workflow, support for many resource types over multiple cloud providers), 6 | but as a programming language it has very limited data structures and abstraction cababilities. With this library 7 | one can leverage haskell abstractions to specifying infrastructure, whilst relying on terraforms excellent features. 8 | 9 | The terraform "API" is very broad, supporting hundreds of resource types accross multiple cloud providers. By its nature 10 | a haskell wrapping of this is involves significant boilerplate. This [boilerplate][] is [generated][] from an API specification. 11 | 12 | Currently only a small subset of the AWS API is implemented, though 13 | the addition of other resources and providers should be 14 | straightforward. 15 | 16 | The [examples][] directory illustrates API usage. 17 | 18 | [terraform]:https://www.terraform.io/ 19 | [boilerplate]:https://github.com/timbod7/terraform-hs/blob/master/src/Language/Terraform/Aws.hs 20 | [generated]:https://github.com/timbod7/terraform-hs/blob/master/scripts/generate.hs 21 | [examples]:https://github.com/timbod7/terraform-hs/blob/master/examples 22 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /examples/tf-example.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | import qualified Data.Map as M 4 | import qualified Data.Text as T 5 | import qualified Language.Terraform.Util.Text as T 6 | 7 | import Control.Lens 8 | import Control.Monad(void) 9 | import Data.Traversable(for) 10 | import Data.Monoid 11 | import Language.Terraform.Core 12 | import Language.Terraform.Aws 13 | 14 | ---------------------------------------------------------------------- 15 | -- Configuration 16 | 17 | s3BucketPrefix :: T.Text 18 | s3BucketPrefix = "com-somebucketprefix-" 19 | 20 | cidrBlock :: CidrBlock 21 | cidrBlock = "10.30.0.0/16" 22 | 23 | azparams :: [(NameElement, [Char], CidrBlock, CidrBlock)] 24 | 25 | 26 | azparams = 27 | [ ("apse2a", "ap-southeast-2a", "10.30.0.0/19", "10.30.32.0/20") 28 | , ("apse2b", "ap-southeast-2b", "10.30.64.0/19", "10.30.96.0/20") 29 | , ("apse2c", "ap-southeast-2c", "10.30.128.0/19", "10.30.160.0/20") 30 | ] 31 | 32 | ---------------------------------------------------------------------- 33 | -- The AWS networking infrastructure 34 | 35 | data AzDetails = AzDetails { 36 | az_name :: T.Text, 37 | az_external_subnet :: AwsSubnet, 38 | az_internal_subnet :: AwsSubnet 39 | } 40 | 41 | data NetworkDetails = NetworkDetails { 42 | nd_vpc :: AwsVpc, 43 | nd_azs :: [AzDetails] 44 | } 45 | 46 | networking :: TF NetworkDetails 47 | networking = do 48 | 49 | vpc <- awsVpc' "vpc" cidrBlock 50 | ig <- awsInternetGateway' "gw" (vpc_id vpc) 51 | rtexternal <- awsRouteTable' "rtexternal" (vpc_id vpc) 52 | void $ awsRoute "r" (rt_id rtexternal) "0.0.0.0/0" 53 | ( set r_gateway_id (Just (ig_id ig)) 54 | ) 55 | 56 | -- Generate resources for each availability zone 57 | azs <- for azparams $ \(azname, availabilityZone,internalCidrBlock,externalCidrBlock) -> do 58 | withNameScope azname $ do 59 | -- External subnet 60 | snexternal <- awsSubnet' "snexternal" (vpc_id vpc) externalCidrBlock 61 | eip <- awsEip "ngeip" 62 | ( set eip_vpc True 63 | ) 64 | ng <- awsNatGateway' "ng" (eip_id eip) (sn_id snexternal) 65 | dependsOn ng ig 66 | void $ awsRouteTableAssociation' "raexternal" (sn_id snexternal) (rt_id rtexternal) 67 | 68 | -- Internal subnet 69 | rtinternal <- awsRouteTable' "rtinternal" (vpc_id vpc) 70 | void $ awsRoute "r1" (rt_id rtinternal) "0.0.0.0/0" 71 | ( set r_nat_gateway_id (Just (ng_id ng)) 72 | ) 73 | sninternal <- awsSubnet' "sninternal" (vpc_id vpc) internalCidrBlock 74 | awsRouteTableAssociation' "rtainternal" (sn_id sninternal) (rt_id rtinternal) 75 | return (AzDetails azname snexternal sninternal) 76 | return (NetworkDetails vpc azs) 77 | 78 | ---------------------------------------------------------------------- 79 | -- An s3 bucket with some content derived from the infrastructure 80 | 81 | s3 :: TF AwsS3Bucket 82 | s3 = do 83 | deployBucket <- awsS3Bucket' "deploy" (s3BucketPrefix <> "shared-deploy") 84 | awsS3BucketObject "tfconfig" (s3_id deployBucket) "shared/config/tf-variables.sh" 85 | ( set s3o_content (Just $ T.unlines 86 | [ "# Shared infrastructure details" 87 | , T.template "TF_OUTPUT_s3_deploy_bucket = \"$1\"" [tfRefText (s3_id deployBucket)] 88 | ]) 89 | ) 90 | return deployBucket 91 | 92 | 93 | ---------------------------------------------------------------------- 94 | -- All "shared" infrastructure 95 | 96 | data SharedInfrastructure = SharedInfrastructure { 97 | si_networkDetails :: NetworkDetails, 98 | si_deployBucket :: AwsS3Bucket, 99 | si_alertTopic :: AwsSnsTopic, 100 | si_alarmTopic :: AwsSnsTopic 101 | } 102 | 103 | namedSnsTopic :: T.Text -> TF AwsSnsTopic 104 | namedSnsTopic n = do 105 | sn <- scopedName n 106 | awsSnsTopic' n sn 107 | 108 | shared :: TF SharedInfrastructure 109 | shared = do 110 | nd <- networking 111 | db <- s3 112 | alarms <- namedSnsTopic "alarms" 113 | alerts <- namedSnsTopic "alerts" 114 | 115 | return SharedInfrastructure 116 | { si_networkDetails = nd 117 | , si_deployBucket = db 118 | , si_alarmTopic = alarms 119 | , si_alertTopic = alerts 120 | } 121 | 122 | ---------------------------------------------------------------------- 123 | -- An application server deployed into the shared infrastructure, 124 | -- with both uat and prod instances 125 | 126 | demoappTags = M.fromList 127 | [ ("tf-stack","demoapp") 128 | , ("cost-center","demoapp") 129 | ] 130 | 131 | ingressOnPort :: Int -> IngressRuleParams 132 | ingressOnPort port = IngressRuleParams 133 | { _ir_from_port = port 134 | , _ir_to_port = port 135 | , _ir_protocol = "tcp" 136 | , _ir_cidr_blocks = ["0.0.0.0/0"] 137 | } 138 | 139 | egressAll :: EgressRuleParams 140 | egressAll = EgressRuleParams 141 | { _er_from_port = 0 142 | , _er_to_port = 0 143 | , _er_protocol = "-1" 144 | , _er_cidr_blocks = ["0.0.0.0/0"] 145 | } 146 | 147 | mkPostgres :: NetworkDetails -> AwsDbSubnetGroup -> DBInstanceClass -> TF AwsDbInstance 148 | mkPostgres nd subnetGroup instanceClass = do 149 | db <- awsDbInstance "db" 5 "postgres" instanceClass "postgres" "password" 150 | ( set db_engine_version "9.4.7" 151 | . set db_publicly_accessible True 152 | . set db_backup_retention_period 3 153 | . set db_db_subnet_group_name (Just (dsg_name subnetGroup)) 154 | ) 155 | output "dbaddress" (tfRefText (db_address db)) 156 | return db 157 | 158 | mkAppServer :: NetworkDetails -> AwsSecurityGroup -> AwsIamInstanceProfile -> InstanceType -> TF AwsInstance 159 | mkAppServer nd securityGroup iamInstanceProfile instanceType = do 160 | ec2 <- awsInstance "appserver" "ami-623c0d01" instanceType 161 | ( set i_tags demoappTags 162 | . set i_subnet_id (Just (sn_id (az_external_subnet (head (nd_azs nd))))) 163 | . set i_vpc_security_group_ids [sg_id securityGroup] 164 | . set i_iam_instance_profile (Just (iamip_id iamInstanceProfile)) 165 | . set i_root_block_device (Just $ makeRootBlockDeviceParams & 166 | ( set rbd_volume_size (Just 20) 167 | )) 168 | ) 169 | eip <- awsEip "appserverip" 170 | ( set eip_instance (Just (i_id ec2)) 171 | . set eip_vpc True 172 | ) 173 | output "appserverip" (tfRefText (eip_public_ip eip)) 174 | return ec2 175 | 176 | iamPolicy = T.intercalate "\n" 177 | [ "{" 178 | , "\"Version\": \"2012-10-17\"," 179 | , "\"Statement\": [" 180 | , " {" 181 | , " \"Action\": \"sts:AssumeRole\"," 182 | , " \"Principal\": { \"Service\": \"ec2.amazonaws.com\" }," 183 | , " \"Effect\": \"Allow\"," 184 | , " \"Sid\": \"\"" 185 | , " }" 186 | , " ]" 187 | , "}" 188 | ] 189 | 190 | publishMetricsPolicy = T.intercalate "\n" 191 | [ "{" 192 | , " \"Statement\": [" 193 | , " {" 194 | , " \"Action\": [" 195 | , " \"cloudwatch:GetMetricStatistics\"," 196 | , " \"cloudwatch:ListMetrics\"," 197 | , " \"cloudwatch:PutMetricData\"," 198 | , " \"ec2:DescribeTags\"" 199 | , " ]," 200 | , " \"Effect\": \"Allow\"," 201 | , " \"Resource\": \"*\"" 202 | , " }" 203 | , " ]" 204 | , "}" 205 | ] 206 | 207 | s3ReadonlyPolicy :: AwsS3Bucket -> T.Text 208 | s3ReadonlyPolicy bucket = T.intercalate "\n" 209 | [ "{" 210 | , " \"Version\": \"2012-10-17\"," 211 | , " \"Statement\": [" 212 | , " {" 213 | , " \"Action\": [" 214 | , " \"s3:GetObject\"" 215 | , " ]," 216 | , " \"Effect\": \"Allow\"," 217 | , " \"Resource\": [" 218 | , T.template 219 | " \"arn:aws:s3:::$1/*\"" 220 | [tfRefText (s3_id bucket)] 221 | , " ]" 222 | , " }" 223 | , " ]" 224 | , "}" 225 | ] 226 | 227 | highDiskAlert:: AwsSnsTopic -> AwsInstance -> TF AwsCloudwatchMetricAlarm 228 | highDiskAlert topic ec2Instance = do 229 | sn <- scopedName "highdisk" 230 | awsCloudwatchMetricAlarm "highdisk" sn "GreaterThanThreshold" 1 "DiskSpaceUtilization" "System/Linux" 300 "Average" 90 231 | ( set cma_dimensions (M.fromList 232 | [ ("InstanceId", tfRefText (i_id ec2Instance) ) 233 | , ("Filesystem", "/dev/xvda1") 234 | , ("MountPath", "/") 235 | ]) 236 | . set cma_alarm_description "Sustained high disk usage for application server" 237 | . set cma_alarm_actions [sns_arn topic] 238 | ) 239 | 240 | highCpuAlert :: AwsSnsTopic -> AwsInstance -> TF AwsCloudwatchMetricAlarm 241 | highCpuAlert topic ec2Instance = do 242 | sn <- scopedName "highcpu" 243 | awsCloudwatchMetricAlarm "highcpu" sn "GreaterThanThreshold" 4 "CPUUtilization" "AWS/EC2" 300 "Average" 90 244 | ( set cma_dimensions (M.fromList 245 | [ ("InstanceId", tfRefText (i_id ec2Instance) ) 246 | ]) 247 | . set cma_alarm_description "Sustained high cpu usage for application server" 248 | . set cma_alarm_actions [sns_arn topic] 249 | ) 250 | 251 | demoapp :: SharedInfrastructure -> TF () 252 | demoapp sharedInfrastructure = do 253 | let networkDetails = si_networkDetails sharedInfrastructure 254 | sg <- awsSecurityGroup "sgappserver" 255 | ( set sg_tags demoappTags 256 | . set sg_vpc_id (Just (vpc_id (nd_vpc networkDetails))) 257 | . set sg_ingress 258 | [ ingressOnPort 22 259 | , ingressOnPort 80 260 | , ingressOnPort 443 261 | ] 262 | . set sg_egress 263 | [ egressAll 264 | ] 265 | ) 266 | 267 | iamr <- awsIamRole' "appserver" iamPolicy 268 | iamip <- awsIamInstanceProfile "appserver" 269 | ( set iamip_roles [iamr_name iamr] 270 | ) 271 | 272 | let namedPolicy name0 policy = do 273 | name <- scopedName name0 274 | awsIamRolePolicy' name0 name policy (iamr_id iamr) 275 | 276 | void $ namedPolicy "publishmetrics" 277 | publishMetricsPolicy 278 | void $ namedPolicy "deployaccess" 279 | (s3ReadonlyPolicy (si_deployBucket sharedInfrastructure)) 280 | 281 | dbsg <- do 282 | sname <- scopedName "dsg" 283 | awsDbSubnetGroup' "dsg" sname 284 | [sn_id (az_external_subnet az) | az <- nd_azs networkDetails] 285 | 286 | withNameScope "prod" $ do 287 | ec2 <- mkAppServer networkDetails sg iamip "t2.medium" 288 | mkPostgres networkDetails dbsg "db.t2.medium" 289 | 290 | highDiskAlert (si_alertTopic sharedInfrastructure) ec2 291 | highCpuAlert (si_alertTopic sharedInfrastructure) ec2 292 | 293 | withNameScope "uat" $ do 294 | ec2 <- mkAppServer networkDetails sg iamip "t2.micro" 295 | mkPostgres networkDetails dbsg "db.t2.micro" 296 | 297 | return () 298 | 299 | ---------------------------------------------------------------------- 300 | -- Combine everything and generate the terraform file 301 | 302 | main = generateFiles "/tmp" $ do 303 | sharedInfrastructure <- withNameScope "shared" $ do 304 | newAws (makeAwsParams "ap-southeast-2") 305 | shared 306 | 307 | withNameScope "demoapp" $ do 308 | demoapp sharedInfrastructure 309 | 310 | -------------------------------------------------------------------------------- /scripts/generate.hs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env stack 2 | {- stack --stack-yaml ./stack.yaml runghc --package terraform-hs -} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | 5 | import qualified Data.Set as S 6 | import qualified Data.Text as T 7 | import qualified Data.Text.IO as T 8 | import qualified Language.Terraform.Util.Text as T 9 | 10 | import Data.List (intercalate, intersperse) 11 | import Data.Monoid 12 | import System.FilePath (()) 13 | 14 | awsHeader :: Code 15 | awsHeader = clines 16 | [ "type AwsRegion = T.Text" 17 | , "data AwsId a = AwsId" 18 | , "type CidrBlock = T.Text" 19 | , "type AvailabilityZone = T.Text" 20 | , "type Ami = T.Text" 21 | , "type InstanceType = T.Text" 22 | , "type KeyName = T.Text" 23 | , "type S3BucketName = T.Text" 24 | , "type S3Key = T.Text" 25 | , "type Arn = T.Text" 26 | , "newtype IpAddress = IpAddress T.Text" 27 | , "type VolumeType = T.Text" 28 | , "type CannedAcl = T.Text" 29 | , "type MetricComparisonOperator = T.Text" 30 | , "type MetricNamespace = T.Text" 31 | , "type MetricName = T.Text" 32 | , "type MetricStatistic = T.Text" 33 | , "type MetricUnit = T.Text" 34 | , "type DBEngine = T.Text" 35 | , "type DBInstanceClass = T.Text" 36 | , "type HostedZoneId = T.Text" 37 | , "type Route53RecordType = T.Text" 38 | , "" 39 | , "-- A typed ARN" 40 | , "newtype AwsArn t = AwsArn {" 41 | , " tArn :: Arn" 42 | , "} deriving (Eq);" 43 | , "" 44 | , "instance ToResourceField (AwsArn t) where" 45 | , " toResourceField (AwsArn t) = toResourceField t" 46 | , "" 47 | , "-- | Add an aws provider to the resource graph." 48 | , "--" 49 | , "-- See the original " 50 | , "-- for details." 51 | , "" 52 | , "newAws :: AwsParams -> TF ()" 53 | , "newAws params =" 54 | , " mkProvider \"aws\" $ catMaybes" 55 | , " [ Just (\"region\", toResourceField (aws_region params))" 56 | , " , let v = aws_access_key params in if v == \"\" then Nothing else (Just (\"access_key\", toResourceField v))" 57 | , " , let v = aws_secret_key params in if v == \"\" then Nothing else (Just (\"secret_key\", toResourceField v))" 58 | , " ]" 59 | , "" 60 | , "data AwsParams = AwsParams" 61 | , " { aws_region :: AwsRegion" 62 | , " , aws_access_key :: T.Text" 63 | , " , aws_secret_key :: T.Text" 64 | , " }" 65 | , "" 66 | , "makeAwsParams :: AwsRegion -> AwsParams" 67 | , "makeAwsParams region = AwsParams region \"\" \"\"" 68 | ] 69 | 70 | awsResources :: [Code] 71 | awsResources = 72 | [resourceCode "aws_vpc" "vpc" 73 | "https://www.terraform.io/docs/providers/aws/d/vpc.html" 74 | [ ("cidr_block", NamedType "CidrBlock", Required) 75 | , ("instance_tenancy", NamedType "T.Text", Optional) 76 | , ("enable_dns_support", NamedType "Bool", OptionalWithDefault "True") 77 | , ("enable_dns_hostnames", NamedType "Bool", OptionalWithDefault "False") 78 | , ("enable_classic_link", NamedType "Bool", OptionalWithDefault "False") 79 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 80 | ] 81 | [ ("id", AwsIdRef "aws_vpc") 82 | ] 83 | 84 | , resourceCode "aws_nat_gateway" "ng" 85 | "https://www.terraform.io/docs/providers/aws/r/nat_gateway.html" 86 | [ ("allocation_id", AwsIdRef "aws_eip", Required) 87 | , ("subnet_id", AwsIdRef "aws_subnet", Required) 88 | ] 89 | [ ("id", AwsIdRef "aws_nat_gateway") 90 | ] 91 | 92 | , resourceCode "aws_internet_gateway" "ig" 93 | "https://www.terraform.io/docs/providers/aws/r/internet_gateway.html" 94 | [ ("vpc_id", AwsIdRef "aws_vpc", Required) 95 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 96 | ] 97 | [ ("id", AwsIdRef "aws_internet_gateway") 98 | ] 99 | 100 | , resourceCode "aws_subnet" "sn" 101 | "https://www.terraform.io/docs/providers/aws/d/subnet.html" 102 | [ ("vpc_id", AwsIdRef "aws_vpc", Required) 103 | , ("cidr_block", NamedType "CidrBlock", Required) 104 | , ("map_public_ip_on_launch", NamedType "Bool", OptionalWithDefault "False") 105 | , ("availability_zone", NamedType "AvailabilityZone", OptionalWithDefault "\"\"") 106 | 107 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 108 | ] 109 | [ ("id", AwsIdRef "aws_subnet") 110 | ] 111 | 112 | , resourceCode "aws_route_table" "rt" 113 | "https://www.terraform.io/docs/providers/aws/r/route_table.html" 114 | [ ("vpc_id", AwsIdRef "aws_vpc", Required) 115 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 116 | ] 117 | [ ("id", AwsIdRef "aws_route_table") 118 | ] 119 | 120 | , resourceCode "aws_route" "r" 121 | "https://www.terraform.io/docs/providers/aws/r/route.html" 122 | [ ("route_table_id", AwsIdRef "aws_route_table", Required) 123 | , ("destination_cidr_block", NamedType "CidrBlock", Required) 124 | , ("nat_gateway_id", AwsIdRef "aws_nat_gateway", Optional) 125 | , ("gateway_id", AwsIdRef "aws_internet_gateway", Optional) 126 | ] 127 | [] 128 | 129 | , resourceCode "aws_route_table_association" "rta" 130 | "https://www.terraform.io/docs/providers/aws/r/route_table_association.html" 131 | [ ("subnet_id", AwsIdRef "aws_subnet", Required) 132 | , ("route_table_id", AwsIdRef "aws_route_table", Required) 133 | ] 134 | [ ("id", AwsIdRef "aws_route_table_association") 135 | ] 136 | 137 | , fieldsCode "IngressRule" "ir" True 138 | [ ("from_port", NamedType "Int", Required) 139 | , ("to_port", NamedType "Int", Required) 140 | , ("protocol", NamedType "T.Text", Required) 141 | , ("cidr_blocks", NamedType "[CidrBlock]", OptionalWithDefault "[]") 142 | ] 143 | 144 | , fieldsCode "EgressRule" "er" True 145 | [ ("from_port", NamedType "Int", Required) 146 | , ("to_port", NamedType "Int", Required) 147 | , ("protocol", NamedType "T.Text", Required) 148 | , ("cidr_blocks", NamedType "[CidrBlock]", OptionalWithDefault "[]") 149 | ] 150 | 151 | , resourceCode "aws_security_group" "sg" 152 | "https://www.terraform.io/docs/providers/aws/r/security_group.html" 153 | [ ("name", NamedType "T.Text", OptionalWithDefault "\"\"") 154 | , ("name_prefix", NamedType "T.Text", OptionalWithDefault "\"\"") 155 | , ("description", NamedType "T.Text", OptionalWithDefault "\"\"") 156 | , ("ingress", NamedType "[IngressRuleParams]", OptionalWithDefault "[]") 157 | , ("egress", NamedType "[EgressRuleParams]", OptionalWithDefault "[]") 158 | , ("vpc_id", AwsIdRef "aws_vpc", Optional) 159 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 160 | ] 161 | [ ("id", AwsIdRef "aws_security_group") 162 | , ("owner_id", TFRef "T.Text") 163 | ] 164 | 165 | , fieldsCode "RootBlockDevice" "rbd" True 166 | [ ("volume_type", NamedType "VolumeType", OptionalWithDefault "\"standard\"") 167 | , ("volume_size", NamedType "Int", Optional) 168 | , ("delete_on_termination", NamedType "Bool", OptionalWithDefault "True") 169 | ] 170 | 171 | , resourceCode "aws_instance" "i" 172 | "https://www.terraform.io/docs/providers/aws/r/instance.html" 173 | [ ("ami", NamedType "Ami", Required) 174 | , ("availability_zone", NamedType "AvailabilityZone", OptionalWithDefault "\"\"") 175 | , ("ebs_optimized", NamedType "Bool", Optional) 176 | , ("instance_type", NamedType "InstanceType", Required) 177 | , ("key_name", NamedType "KeyName", Optional) 178 | , ("monitoring", NamedType "Bool", OptionalWithDefault "True") 179 | , ("subnet_id", AwsIdRef "aws_subnet", Optional) 180 | , ("associate_public_ip_address", NamedType "Bool", Optional) 181 | , ("root_block_device", NamedType "RootBlockDeviceParams", Optional) 182 | , ("user_data", NamedType "T.Text", OptionalWithDefault "\"\"") 183 | , ("iam_instance_profile", AwsIdRef "aws_iam_instance_profile", Optional) 184 | , ("vpc_security_group_ids", FTList (AwsIdRef "aws_security_group"), OptionalWithDefault "[]") 185 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 186 | ] 187 | [ ("id", AwsIdRef "aws_instance") 188 | , ("public_ip", TFRef "IpAddress") 189 | , ("private_ip", TFRef "IpAddress") 190 | ] 191 | 192 | , resourceCode "aws_launch_configuration" "lc" 193 | "https://www.terraform.io/docs/providers/aws/r/launch_configuration.html" 194 | [ ("name'", NamedType "T.Text", OptionalWithDefault "\"\"") 195 | , ("name_prefix", NamedType "T.Text", OptionalWithDefault "\"\"") 196 | , ("image_id", NamedType "Ami", Required) 197 | , ("instance_type", NamedType "InstanceType", Required) 198 | , ("iam_instance_profile", AwsIdRef "aws_iam_instance_profile", Optional) 199 | , ("key_name", NamedType "KeyName", Optional) 200 | , ("security_groups", FTList (AwsIdRef "aws_security_group"), OptionalWithDefault "[]") 201 | , ("associate_public_ip_address", NamedType "Bool", Optional) 202 | , ("user_data", NamedType "T.Text", OptionalWithDefault "\"\"") 203 | , ("enable_monitoring", NamedType "Bool", OptionalWithDefault "True") 204 | , ("ebs_optimized", NamedType "Bool", Optional) 205 | , ("root_block_device", NamedType "RootBlockDeviceParams", Optional) 206 | ] 207 | [ ("id", AwsIdRef "aws_launch_configuration") 208 | , ("name", TFRef "T.Text") 209 | ] 210 | 211 | , resourceCode "aws_autoscaling_group" "ag" 212 | "https://www.terraform.io/docs/providers/aws/r/autoscaling_group.html" 213 | [ ("name'", NamedType "T.Text", OptionalWithDefault "\"\"") 214 | , ("name_prefix", NamedType "T.Text", OptionalWithDefault "\"\"") 215 | , ("min_size", NamedType "Int", Required) 216 | , ("max_size", NamedType "Int", Required) 217 | , ("vpc_zone_identifier", FTList (AwsIdRef "aws_subnet"), OptionalWithDefault "[]") 218 | , ("launch_configuration", TFRef "T.Text", Required) 219 | , ("load_balancers", FTList (TFRef "T.Text"), OptionalWithDefault "[]") 220 | , ("tag", FTList (NamedType "AsgTagParams"), ExpandedList) 221 | ] 222 | [ ("id", AwsIdRef "aws_autoscaling_group") 223 | , ("arn", TFRef "Arn") 224 | , ("name", TFRef "T.Text") 225 | ] 226 | 227 | , fieldsCode "AsgTag" "asg" True 228 | [ ("key", NamedType "T.Text", Required) 229 | , ("value", NamedType "T.Text", Required) 230 | , ("propagate_at_launch", NamedType "Bool", Required) 231 | ] 232 | 233 | , resourceCode "aws_eip" "eip" 234 | "https://www.terraform.io/docs/providers/aws/r/eip.html" 235 | [ ("vpc", NamedType "Bool", OptionalWithDefault "False") 236 | , ("instance", AwsIdRef "aws_instance", Optional) 237 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 238 | ] 239 | [ ("id", AwsIdRef "aws_eip") 240 | , ("private_ip", TFRef "IpAddress") 241 | , ("public_ip", TFRef "IpAddress") 242 | ] 243 | 244 | , fieldsCode "AccessLogs" "al" True 245 | [ ("bucket", NamedType "S3BucketName", Required) 246 | , ("bucket_prefix", NamedType "S3Key", OptionalWithDefault "\"\"") 247 | , ("interval", NamedType "Int", OptionalWithDefault "60") 248 | , ("enabled", NamedType "Bool", OptionalWithDefault "True") 249 | ] 250 | 251 | , fieldsCode "Listener" "l" True 252 | [ ("instance_port", NamedType "Int", Required) 253 | , ("instance_protocol", NamedType "T.Text", Required) 254 | , ("lb_port", NamedType "Int", Required) 255 | , ("lb_protocol", NamedType "T.Text", Required) 256 | , ("ssl_certificate_id", NamedType "Arn", Optional) 257 | ] 258 | 259 | , fieldsCode "HealthCheck" "hc" True 260 | [ ("healthy_threshold", NamedType "Int", Required) 261 | , ("unhealthy_threshold", NamedType "Int", Required) 262 | , ("target", NamedType "T.Text", Required) 263 | , ("interval", NamedType "Int", Required) 264 | , ("timeout", NamedType "Int", Required) 265 | ] 266 | 267 | , resourceCode "aws_elb" "elb" 268 | "https://www.terraform.io/docs/providers/aws/r/elb.html" 269 | [ ("name'", NamedType "T.Text", Optional) 270 | , ("access_logs", NamedType "AccessLogsParams", Optional) 271 | , ("security_groups", FTList (AwsIdRef "aws_security_group"), OptionalWithDefault "[]") 272 | , ("subnets", FTList (AwsIdRef "aws_subnet"), OptionalWithDefault "[]") 273 | , ("instances", FTList (AwsIdRef "aws_instance"), OptionalWithDefault "[]") 274 | , ("listener", FTList (NamedType "ListenerParams"), Required) 275 | , ("health_check", NamedType "HealthCheckParams", Optional) 276 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 277 | ] 278 | [ ("id", TFRef "T.Text") 279 | , ("name", TFRef "T.Text") 280 | , ("dns_name", TFRef "T.Text") 281 | , ("zone_id", TFRef "T.Text") 282 | ] 283 | 284 | , fieldsCode "BucketVersioning" "bv" True 285 | [ ("enabled", NamedType "Bool", OptionalWithDefault "False") 286 | , ("mfa_delete", NamedType "Bool", OptionalWithDefault "False") 287 | ] 288 | 289 | , fieldsCode "Expiration" "e" True 290 | [ ("days", NamedType "Int", Optional) 291 | , ("date", NamedType "T.Text", Optional) 292 | , ("expired_object_delete_marker", NamedType "Bool", OptionalWithDefault "False") 293 | ] 294 | 295 | , fieldsCode "LifecycleRule" "lr" True 296 | [ ("id", NamedType "T.Text", Optional) 297 | , ("prefix", NamedType "T.Text", Required) 298 | , ("enabled", NamedType "Bool", Required) 299 | , ("expiration", NamedType "ExpirationParams", Optional) 300 | ] 301 | 302 | , resourceCode "aws_elb_attachment" "elba" 303 | "https://www.terraform.io/docs/providers/aws/r/elb_attachment.html" 304 | [ ("elb", NamedType "T.Text", Required) 305 | , ("instance", NamedType "T.Text", Required) 306 | ] 307 | [ 308 | ] 309 | 310 | , resourceCode "aws_autoscaling_attachment" "asa" 311 | "https://www.terraform.io/docs/providers/aws/r/autoscaling_attachment.html" 312 | [ ("autoscaling_group_name", NamedType "T.Text", Required) 313 | , ("elb", NamedType "T.Text", Optional) 314 | , ("alb_target_group_arn", NamedType "AwsArn AwsLbTargetGroup", Optional) 315 | ] 316 | [ 317 | ] 318 | 319 | , fieldsCode "CorsRule" "cors" True 320 | [ ("allowed_headers", FTList (NamedType "T.Text"), OptionalWithDefault "[]") 321 | , ("allowed_methods", FTList (NamedType "T.Text"), Required) 322 | , ("allowed_origins", FTList (NamedType "T.Text"), Required) 323 | , ("expose_headers", FTList (NamedType "T.Text"), OptionalWithDefault "[]") 324 | , ("max_age_seconds", NamedType "Int", Optional) 325 | ] 326 | 327 | , resourceCode "aws_s3_bucket" "s3" 328 | "https://www.terraform.io/docs/providers/aws/r/s3_bucket.html" 329 | [ ("bucket", NamedType "T.Text", Required) 330 | , ("acl", NamedType "CannedAcl", OptionalWithDefault "\"private\"") 331 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 332 | , ("versioning", NamedType "BucketVersioningParams", Optional) 333 | , ("lifecycle_rule", NamedType "LifecycleRuleParams", Optional) 334 | , ("cors_rule", NamedType "CorsRuleParams", Optional) 335 | ] 336 | [ ("id", TFRef "S3BucketName") 337 | ] 338 | 339 | , resourceCode "aws_s3_bucket_object" "s3o" 340 | "https://www.terraform.io/docs/providers/aws/d/s3_bucket_object.html" 341 | [ ("bucket", TFRef "S3BucketName", Required) 342 | , ("key", NamedType "S3Key", Required) 343 | , ("source", NamedType" FilePath", Optional) 344 | , ("content", NamedType" T.Text", Optional) 345 | ] 346 | [ ("id", TFRef "T.Text") 347 | , ("etag", TFRef "T.Text") 348 | , ("version_id", TFRef "T.Text") 349 | ] 350 | 351 | , resourceCode "aws_iam_user" "iamu" 352 | "https://www.terraform.io/docs/providers/aws/r/iam_user.html" 353 | [ ("name'", NamedType "T.Text", Required) 354 | , ("path", NamedType "T.Text", OptionalWithDefault "\"/\"") 355 | , ("force_destroy", NamedType "Bool", OptionalWithDefault "False") 356 | ] 357 | [ ("arn", TFRef "Arn") 358 | , ("name", TFRef "T.Text") 359 | , ("unique_id", TFRef "T.Text") 360 | ] 361 | 362 | , resourceCode "aws_iam_user_policy" "iamup" 363 | "https://www.terraform.io/docs/providers/aws/r/iam_user_policy.html" 364 | [ ("name", NamedType "T.Text", Required) 365 | , ("policy", NamedType "T.Text", Required) 366 | , ("user", TFRef "T.Text", Required) 367 | ] 368 | [] 369 | 370 | , resourceCode "aws_iam_user_policy_attachment" "iamupa" 371 | "https://www.terraform.io/docs/providers/aws/r/iam_user_policy_attachment.html" 372 | [ ("user", TFRef "T.Text", Required) 373 | , ("policy_arn", NamedType "T.Text", Required) 374 | ] 375 | [] 376 | 377 | , resourceCode "aws_iam_role" "iamr" 378 | "https://www.terraform.io/docs/providers/aws/r/iam_role.html" 379 | [ ("name'", NamedType "T.Text", OptionalWithDefault "\"\"") 380 | , ("name_prefix", NamedType "T.Text", OptionalWithDefault "\"\"") 381 | , ("assume_role_policy", NamedType "T.Text", Required) 382 | , ("path", NamedType "T.Text", OptionalWithDefault "\"\"") 383 | ] 384 | [ ("id", AwsIdRef "aws_iam_role") 385 | , ("arn", TFRef "Arn") 386 | , ("name", TFRef "T.Text") 387 | , ("create_date", TFRef "T.Text") 388 | , ("unique_id", TFRef "T.Text") 389 | ] 390 | 391 | , resourceCode "aws_iam_instance_profile" "iamip" 392 | "https://www.terraform.io/docs/providers/aws/r/iam_instance_profile.html" 393 | [ ("name", NamedType "T.Text", OptionalWithDefault "\"\"") 394 | , ("name_prefix", NamedType "T.Text", OptionalWithDefault "\"\"") 395 | , ("path", NamedType "T.Text", OptionalWithDefault "\"/\"") 396 | , ("roles", FTList (TFRef "T.Text"), OptionalWithDefault "[]") 397 | , ("role", TFRef "T.Text", Optional) 398 | ] 399 | [ ("id", AwsIdRef "aws_iam_instance_profile") 400 | , ("arn", TFRef "Arn") 401 | , ("create_date", TFRef "T.Text") 402 | , ("unique_id", TFRef "T.Text") 403 | ] 404 | 405 | , resourceCode "aws_iam_role_policy" "iamrp" 406 | "https://www.terraform.io/docs/providers/aws/r/iam_role_policy.html" 407 | [ ("name", NamedType "T.Text", Required) 408 | , ("policy", NamedType "T.Text", Required) 409 | , ("role", AwsIdRef "aws_iam_role", Required) 410 | ] 411 | [ ("id", AwsIdRef "aws_iam_instance_profile") 412 | ] 413 | 414 | , resourceCode "aws_sns_topic" "sns" 415 | "https://www.terraform.io/docs/providers/aws/r/sns_topic.html" 416 | [ ("name", NamedType "T.Text", Required) 417 | , ("display_name", NamedType "T.Text", OptionalWithDefault "\"\"") 418 | ] 419 | [ ("id", AwsIdRef "aws_sns_topic") 420 | , ("arn", TFRef "Arn") 421 | ] 422 | 423 | , resourceCode "aws_cloudwatch_metric_alarm" "cma" 424 | "https://www.terraform.io/docs/providers/aws/r/cloudwatch_metric_alarm.html" 425 | [ ("alarm_name", NamedType "T.Text", Required) 426 | , ("comparison_operator", NamedType "MetricComparisonOperator", Required) 427 | , ("evaluation_periods", NamedType "Int", Required) 428 | , ("metric_name", NamedType "MetricName", Required) 429 | , ("namespace", NamedType "MetricNamespace", Required) 430 | , ("period", NamedType "Int", Required) 431 | , ("statistic", NamedType "MetricStatistic", Required) 432 | , ("threshold", NamedType "Int", Required) 433 | , ("actions_enabled", NamedType "Bool", OptionalWithDefault "True") 434 | , ("alarm_actions", FTList (TFRef "Arn"), OptionalWithDefault "[]") 435 | , ("alarm_description", NamedType "T.Text", OptionalWithDefault "\"\"") 436 | , ("dimensions", TagsMap, OptionalWithDefault "M.empty") 437 | , ("insufficient_data_actions", FTList (TFRef "Arn"), OptionalWithDefault "[]") 438 | , ("ok_actions", FTList (TFRef "Arn"), OptionalWithDefault "[]") 439 | , ("unit", NamedType "MetricUnit", OptionalWithDefault "\"\"") 440 | ] 441 | [ ("id", AwsIdRef "aws_cloudwatch_metric_alarm") 442 | ] 443 | 444 | , resourceCode "aws_rds_cluster" "rc" 445 | "https://www.terraform.io/docs/providers/aws/r/rds_cluster.html" 446 | [ ("cluster_identifier", NamedType "T.Text", OptionalWithDefault "\"\"") 447 | , ("engine", NamedType "DBEngine", Required) 448 | , ("engine_version", NamedType "T.Text", Optional) 449 | , ("database_name'", NamedType "T.Text", OptionalWithDefault "\"\"") 450 | , ("port'", NamedType "Int", Optional) 451 | , ("master_username'", NamedType "T.Text", Required) 452 | , ("master_password", NamedType "T.Text", Required) 453 | , ("apply_immediately", NamedType "Bool", OptionalWithDefault "False") 454 | , ("skip_final_snapshot", NamedType "Bool", OptionalWithDefault "False") 455 | , ("final_snapshot_identifier", NamedType "T.Text", Optional) 456 | , ("vpc_security_group_ids", FTList (AwsIdRef "aws_security_group"), OptionalWithDefault "[]") 457 | , ("db_cluster_parameter_group_name", TFRef "T.Text", Optional) 458 | , ("db_subnet_group_name", TFRef "T.Text", Optional) 459 | , ("backup_retention_period", NamedType "Int", OptionalWithDefault "0") 460 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 461 | ] 462 | [ ("id", AwsIdRef "aws_rds_cluster") 463 | , ("reader_endpoint", TFRef "T.Text") 464 | , ("endpoint", TFRef "T.Text") 465 | , ("database_name", TFRef "T.Text") 466 | , ("port", TFRef "T.Text") 467 | , ("master_username", TFRef "T.Text") 468 | ] 469 | 470 | , resourceCode "aws_rds_cluster_instance" "rci" 471 | "https://www.terraform.io/docs/providers/aws/r/rds_cluster_instance.html" 472 | [ ("cluster_identifier", NamedType "T.Text", Required) 473 | , ("count", NamedType "Int", Optional) 474 | , ("identifier", NamedType "T.Text", Optional) 475 | , ("instance_class", NamedType "DBInstanceClass", Required) 476 | , ("db_parameter_group_name", TFRef "T.Text", Optional) 477 | ] 478 | [ ("id", AwsIdRef "aws_rds_cluster_instance") 479 | ] 480 | 481 | , resourceCode "aws_rds_cluster_parameter_group" "rcpg" 482 | "https://www.terraform.io/docs/providers/aws/r/rds_cluster_parameter_group.html" 483 | [ ("name'", NamedType "T.Text", Required) 484 | , ("family", NamedType "T.Text", Required) 485 | , ("parameter", FTList (NamedType "RcpgParameterParams"), ExpandedList) 486 | ] 487 | [ ("id", AwsIdRef "aws_db_parameter_group") 488 | , ("name", TFRef "T.Text") 489 | ] 490 | 491 | , fieldsCode "RcpgParameter" "rcpgp" True 492 | [ ("name", NamedType "T.Text", Required) 493 | , ("value", NamedType "T.Text", Required) 494 | ] 495 | 496 | , resourceCode "aws_db_instance" "db" 497 | "https://www.terraform.io/docs/providers/aws/r/db_instance.html" 498 | [ ("allocated_storage", NamedType "Int", Required) 499 | , ("engine", NamedType "DBEngine", Required) 500 | , ("engine_version", NamedType "T.Text", OptionalWithDefault "\"\"") 501 | , ("identifier", NamedType "T.Text", OptionalWithDefault "\"\"") 502 | , ("instance_class", NamedType "DBInstanceClass", Required) 503 | , ("name'", NamedType "T.Text", OptionalWithDefault "\"\"") 504 | , ("port'", NamedType "Int", Optional) 505 | , ("username'", NamedType "T.Text", Required) 506 | , ("password", NamedType "T.Text", Required) 507 | , ("publicly_accessible", NamedType "Bool", OptionalWithDefault "False") 508 | , ("backup_retention_period", NamedType "Int", OptionalWithDefault "0") 509 | , ("vpc_security_group_ids", FTList (AwsIdRef "aws_security_group"), OptionalWithDefault "[]") 510 | , ("parameter_group_name", TFRef "T.Text", Optional) 511 | , ("db_subnet_group_name", TFRef "T.Text", Optional) 512 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 513 | , ("skip_final_snapshot", NamedType "Bool", OptionalWithDefault "False") 514 | , ("final_snapshot_identifier", NamedType "T.Text", Optional) 515 | ] 516 | [ ("id", AwsIdRef "aws_db_instance") 517 | , ("arn", TFRef "Arn") 518 | , ("name", TFRef "T.Text") 519 | , ("address", TFRef "T.Text") 520 | , ("port", TFRef "T.Text") 521 | , ("username", TFRef "T.Text") 522 | ] 523 | 524 | , resourceCode "aws_db_parameter_group" "dbpg" 525 | "https://www.terraform.io/docs/providers/aws/r/db_parameter_group.html" 526 | [ ("name'", NamedType "T.Text", Required) 527 | , ("family", NamedType "T.Text", Required) 528 | , ("parameter", FTList (NamedType "DbpgParameterParams"), ExpandedList) 529 | ] 530 | [ ("id", AwsIdRef "aws_db_parameter_group") 531 | , ("name", TFRef "T.Text") 532 | ] 533 | 534 | , fieldsCode "DbpgParameter" "dbpgp" True 535 | [ ("name", NamedType "T.Text", Required) 536 | , ("value", NamedType "T.Text", Required) 537 | , ("apply_method", NamedType "T.Text", Optional) 538 | ] 539 | 540 | , resourceCode "aws_db_subnet_group" "dsg" 541 | "https://www.terraform.io/docs/providers/aws/r/db_subnet_group.html" 542 | [ ("name'", NamedType "T.Text", Required) 543 | , ("description", NamedType "T.Text", OptionalWithDefault "\"\"") 544 | , ("subnet_ids", FTList (AwsIdRef "aws_subnet"), Required) 545 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 546 | ] 547 | [ ("id", AwsIdRef "aws_db_subnet_group") 548 | , ("name", TFRef "T.Text") 549 | , ("arn", TFRef "Arn") 550 | ] 551 | 552 | , resourceCode "aws_route53_zone" "r53z" 553 | "https://www.terraform.io/docs/providers/aws/r/route53_zone.html" 554 | [ ("name", NamedType "T.Text", Required) 555 | , ("comment", NamedType "T.Text", OptionalWithDefault "\"Managed by Terraform\"") 556 | , ("vpc_id", AwsIdRef "aws_vpc", Optional) 557 | , ("vpc_region", NamedType "AwsRegion", Optional) 558 | , ("force_destroy", NamedType "Bool", OptionalWithDefault "False") 559 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 560 | ] 561 | [ ("zone_id", TFRef "HostedZoneId") 562 | ] 563 | 564 | , fieldsCode "Route53Alias" "r53a" True 565 | [ ("zone_id", TFRef "HostedZoneId", Required) 566 | , ("name", TFRef "T.Text", Required) 567 | , ("evaluate_target_health", NamedType "Bool", Required) 568 | ] 569 | 570 | , resourceCode "aws_route53_record" "r53r" 571 | "https://www.terraform.io/docs/providers/aws/r/route53_record.html" 572 | [ ("zone_id", TFRef "HostedZoneId", Required) 573 | , ("name", NamedType "T.Text", Required) 574 | , ("type", NamedType "Route53RecordType", Required) 575 | , ("ttl", NamedType "Int", Optional) 576 | , ("records", FTList (TFRef "IpAddress"), OptionalWithDefault "[]") 577 | , ("alias", NamedType "Route53AliasParams", Optional) 578 | ] 579 | [ ("fqdn", TFRef "T.Text") 580 | ] 581 | 582 | , resourceCode "aws_sqs_queue" "sqs" 583 | "https://www.terraform.io/docs/providers/aws/r/sqs_queue.html" 584 | [ ("name", NamedType "T.Text", Required) 585 | , ("visibility_timeout_seconds", NamedType "Int", OptionalWithDefault "30") 586 | , ("message_retention_seconds", NamedType "Int", OptionalWithDefault "345600") 587 | , ("max_message_size", NamedType "Int", OptionalWithDefault "262144") 588 | , ("delay_seconds", NamedType "Int", OptionalWithDefault "0") 589 | , ("receive_wait_time_seconds", NamedType "Int", OptionalWithDefault "0") 590 | , ("policy", NamedType "T.Text", Optional) 591 | , ("redrive_policy", NamedType "T.Text", Optional) 592 | , ("fifo_queue", NamedType "Bool", OptionalWithDefault "False") 593 | , ("content_based_deduplication", NamedType "Bool", OptionalWithDefault "False") 594 | ] 595 | [ ("id", AwsIdRef "aws_sqs_queue") 596 | , ("arn", TFRef "Arn") 597 | ] 598 | 599 | , resourceCode "aws_sqs_queue_policy" "sqsp" 600 | "https://www.terraform.io/docs/providers/aws/r/sqs_queue_policy.html" 601 | [ ("queue_url", NamedType "T.Text", Required) 602 | , ("policy", NamedType "T.Text", Required) 603 | ] 604 | [ 605 | ] 606 | 607 | , resourceCode "aws_ecr_repository" "ecr" 608 | "https://www.terraform.io/docs/providers/aws/r/ecr_repository.html" 609 | [ ("name'", NamedType "T.Text", Required) 610 | ] 611 | [ ("arn", TFRef "Arn") 612 | , ("name", TFRef "T.Text") 613 | , ("registry_id", TFRef "T.Text") 614 | , ("repository_url", TFRef "T.Text") 615 | ] 616 | 617 | , resourceCode "aws_cloudwatch_log_group" "cwlg" 618 | "https://www.terraform.io/docs/providers/aws/r/cloudwatch_log_group.html" 619 | [ ("name'", NamedType "T.Text", Required) 620 | , ("name_prefix'", NamedType "T.Text", Optional) 621 | , ("retention_in_days'", NamedType "T.Text", Optional) 622 | , ("kms_key_id", NamedType "T.Text", Optional) 623 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 624 | ] 625 | [ ("arn", TFRef "Arn") 626 | ] 627 | 628 | , fieldsCode "EbsOptions" "edeo" True 629 | [ ("ebs_enabled", NamedType "Bool", Required) 630 | , ("volume_type", NamedType "T.Text", Optional) 631 | , ("volume_size", NamedType "Int", Optional) 632 | , ("iops", NamedType "Int", Optional) 633 | ] 634 | 635 | , fieldsCode "ClusterConfig" "edcc" True 636 | [ ("instance_type", NamedType "InstanceType", Optional) 637 | , ("instance_count", NamedType "Int", Optional) 638 | , ("dedicated_master_enabled", NamedType "Bool", Optional) 639 | , ("dedicated_master_type", NamedType "InstanceType", Optional) 640 | , ("dedicated_master_count", NamedType "Int", Optional) 641 | , ("zone_awareness_enabled", NamedType "Bool", Optional) 642 | ] 643 | 644 | , fieldsCode "SnapshotOptions" "edso" True 645 | [ ("automated_snapshot_start_hour", NamedType "Int", Required) 646 | ] 647 | 648 | , resourceCode "aws_elasticsearch_domain" "ed" 649 | "https://www.terraform.io/docs/providers/aws/r/elasticsearch_domain.html" 650 | [ ("domain_name'", NamedType "T.Text", Required) 651 | , ("access_policies", NamedType "T.Text", Optional) 652 | , ("advanced_options", NamedType "M.Map T.Text T.Text", OptionalWithDefault "M.empty") 653 | , ("ebs_options", NamedType "EbsOptionsParams", Optional) 654 | , ("cluster_config", NamedType "ClusterConfigParams", Optional) 655 | , ("snapshot_options", NamedType "SnapshotOptionsParams", Optional) 656 | , ("elasticsearch_version", NamedType "T.Text" ,OptionalWithDefault "\"1.5\"") 657 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 658 | ] 659 | [ ("arn", TFRef "Arn") 660 | , ("domain_id", TFRef "T.Text") 661 | , ("domain_name", TFRef "T.Text") 662 | , ("endpoint", TFRef "T.Text") 663 | ] 664 | 665 | , resourceCode "aws_elasticsearch_domain_policy" "edp" 666 | "https://www.terraform.io/docs/providers/aws/r/elasticsearch_domain_policy.html" 667 | [ ("domain_name", NamedType "T.Text", Required) 668 | , ("access_policies", NamedType "T.Text", Required) 669 | ] 670 | [ ("arn", TFRef "Arn") 671 | ] 672 | 673 | , resourceCode "aws_lb" "lb" 674 | "https://www.terraform.io/docs/providers/aws/r/lb.html" 675 | [ ("name", NamedType "T.Text", Optional) 676 | , ("name_prefix", NamedType "T.Text", Optional) 677 | , ("internal", NamedType "Bool", OptionalWithDefault "False") 678 | , ("load_balancer_type", NamedType "LoadBalancerType", OptionalWithDefault "LB_application") 679 | , ("security_groups", FTList (AwsIdRef "aws_security_group"), OptionalWithDefault "[]") 680 | , ("access_logs", NamedType "AccessLogsParams", Optional) 681 | , ("subnets", FTList (AwsIdRef "aws_subnet"), OptionalWithDefault "[]") 682 | , ("subnet_mapping", NamedType "SubnetMappingParams", Optional) 683 | , ("idle_timeout", NamedType "Int", OptionalWithDefault "60") 684 | , ("enable_deletion_protection", NamedType "Bool", OptionalWithDefault "False") 685 | , ("enable_cross_zone_load_balancing", NamedType "Bool", OptionalWithDefault "False") 686 | , ("enable_http2", NamedType "Bool", OptionalWithDefault "True") 687 | , ("ip_address_type", NamedType "T.Text", Optional) 688 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 689 | ] 690 | [ ("id", AwsIdRef "aws_lb") 691 | , ("arn", TFRef "AwsArn AwsLb") 692 | , ("dns_name", TFRef "T.Text") 693 | , ("canonical_hosted_zone_id", TFRef "T.Text") 694 | , ("zone_id", TFRef "T.Text") 695 | ] 696 | 697 | , fieldsCode "SubnetMapping" "sn" True 698 | [ ("subnet_id", AwsIdRef "aws_subnet", Required) 699 | , ("allocation_id", AwsIdRef "aws_eip", Required) 700 | ] 701 | 702 | , resourceCode "aws_lb_listener" "lbl" 703 | "https://www.terraform.io/docs/providers/aws/r/lb_listener.html" 704 | [ ("load_balancer_arn", NamedType "AwsArn AwsLb", Required) 705 | , ("port'", NamedType "Int", Required) 706 | , ("protocol", NamedType "LoadBalancerProtocol", OptionalWithDefault "LB_HTTP") 707 | , ("ssl_policy", NamedType "T.Text", Optional) 708 | , ("certificate_arn", NamedType "AwsArn AwsAcmCertificate", Optional) 709 | , ("default_action", NamedType "ListenerActionParams", Required) 710 | ] 711 | [ ("id", AwsIdRef "aws_lb_listener") 712 | , ("arn", TFRef "AwsArn AwsLbListener") 713 | ] 714 | 715 | , enumCode "LoadBalancerType" "LB" ["application","network"] 716 | , enumCode "LoadBalancerProtocol" "LB" ["TCP","HTTP", "HTTPS"] 717 | 718 | , fieldsCode "ListenerAction" "la" True 719 | [ ("target_group_arn", NamedType "AwsArn AwsLbTargetGroup", Required) 720 | , ("type", NamedType "ListenerActionType", Required) 721 | ] 722 | 723 | , enumCode "ListenerActionType" "LA" ["forward"] 724 | 725 | , resourceCode "aws_lb_target_group" "lbtg" 726 | "https://www.terraform.io/docs/providers/aws/r/lb_target_group.html" 727 | [ ("name'", NamedType "T.Text", Optional) 728 | , ("name_prefix", NamedType "T.Text", Optional) 729 | , ("port", NamedType "Int", Required) 730 | , ("protocol", NamedType "LoadBalancerProtocol", Required) 731 | , ("vpc_id", AwsIdRef "aws_vpc", Required) 732 | , ("deregistration_delay", NamedType "Int", OptionalWithDefault "300") 733 | , ("slow_start", NamedType "Int", OptionalWithDefault "0") 734 | , ("proxy_protocol_v2", NamedType "Bool", Optional) 735 | , ("stickiness", NamedType "TargetGroupStickinessParams", Optional) 736 | , ("health_check", NamedType "TargetGroupHealthCheckParams", Optional) 737 | , ("target_type", NamedType "TargetGroupTargetType", OptionalWithDefault "TG_instance") 738 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 739 | ] 740 | [ ("id", AwsIdRef "aws_lb_target_group") 741 | , ("arn", TFRef "AwsArn AwsLbTargetGroup") 742 | , ("name", TFRef "T.Text") 743 | ] 744 | 745 | , enumCode "TargetGroupTargetType" "TG" ["instance", "ip"] 746 | 747 | , fieldsCode "TargetGroupStickiness" "tgs" True 748 | [ ("type", NamedType "TargetGroupStickinessType", Required) 749 | , ("cookie_duration", NamedType "Int", OptionalWithDefault "86400") 750 | , ("enabled", NamedType "Bool", OptionalWithDefault "True") 751 | ] 752 | 753 | , enumCode "TargetGroupStickinessType" "TG" ["lb_cookie"] 754 | 755 | , fieldsCode "TargetGroupHealthCheck" "tghc" True 756 | [ ("interval", NamedType "Int", OptionalWithDefault "30") 757 | , ("path", NamedType "T.Text", Optional) 758 | , ("port", NamedType "T.Text", OptionalWithDefault "\"traffic-port\"") 759 | , ("protocol", NamedType "LoadBalancerProtocol", OptionalWithDefault "LB_HTTP") 760 | , ("timeout", NamedType "Int", OptionalWithDefault "5") 761 | , ("healthy_threshold", NamedType "Int", OptionalWithDefault "3") 762 | , ("unhealthy_threshold", NamedType "Int", OptionalWithDefault "3") 763 | , ("matcher", NamedType "T.Text", Optional) 764 | ] 765 | 766 | , resourceCode "aws_lb_target_group_attachment" "lbtga" 767 | "https://www.terraform.io/docs/providers/aws/r/lb_target_group_attachment.html" 768 | [ ("target_group_arn", NamedType "AwsArn AwsLbTargetGroup", Required) 769 | , ("target_id", NamedType "T.Text", Required) 770 | , ("port", NamedType "Int", Optional) 771 | , ("availability_zone", NamedType "AvailabilityZone", Optional) 772 | ] 773 | [ ("id", AwsIdRef "aws_lb_target_group_attachment") 774 | ] 775 | 776 | , resourceCode "aws_lb_listener_rule" "lblr" 777 | "https://www.terraform.io/docs/providers/aws/r/lb_listener_rule.html" 778 | [ ("listener_arn", NamedType "AwsArn AwsLbListener", Required) 779 | , ("priority", NamedType "Int", Optional) 780 | , ("action", NamedType "ListenerActionParams", Required) 781 | , ("condition", NamedType "ListenerConditionParams", Required) 782 | ] 783 | [ ("id", AwsIdRef "aws_lb_target_group_attachment") 784 | , ("arn", TFRef "AwsArn AwsLbListenerRule") 785 | ] 786 | 787 | , fieldsCode "ListenerCondition" "lblrc" True 788 | [ ("field", NamedType "ListenerConditionField", Required) 789 | , ("values", FTList (NamedType "T.Text"), Required) 790 | ] 791 | 792 | , enumCode "ListenerConditionField" "LCF" ["path-pattern","host-header"] 793 | 794 | , resourceCode "aws_acm_certificate" "ac" 795 | "https://www.terraform.io/docs/providers/aws/d/acm_certificate.html" 796 | [ ("domain_name", NamedType "T.Text", Required) 797 | , ("subject_alternative_names", FTList (NamedType "T.Text"), OptionalWithDefault "[]") 798 | , ("validation_method", NamedType "CertValidationMethod", Required) 799 | , ("tags", TagsMap, OptionalWithDefault "M.empty") 800 | ] 801 | [ ("id", AwsIdRef "aws_acm_certificate") 802 | , ("arn", TFRef "AwsArn AwsAcmCertificate") 803 | ] 804 | 805 | , enumCode "CertValidationMethod" "CVM" ["DNS","EMAIL","NONE"] 806 | 807 | , resourceCode "aws_acm_certificate_validation" "acv" 808 | "https://www.terraform.io/docs/providers/aws/r/acm_certificate_validation.html" 809 | [ ("certificate_arn", NamedType "AwsArn AwsAcmCertificate", Required) 810 | , ("validation_record_fqdns", FTList (NamedType "T.Text"), OptionalWithDefault "[]") 811 | ] 812 | [ 813 | ] 814 | ] 815 | 816 | data FieldType = NamedType T.Text | TFRef T.Text | AwsIdRef T.Text | FTList FieldType | TagsMap 817 | data FieldMode = Required | Optional | OptionalWithDefault T.Text | ExpandedList 818 | 819 | data Code = CEmpty 820 | | CLine T.Text 821 | | CAppend Code Code 822 | | CIndent Code 823 | 824 | instance Monoid Code where 825 | mempty = CEmpty 826 | mappend = CAppend 827 | 828 | codeText :: Code -> [T.Text] 829 | codeText c = mkLines "" c 830 | where 831 | mkLines :: T.Text -> Code -> [T.Text] 832 | mkLines i CEmpty = [] 833 | mkLines i (CAppend c1 c2) = mkLines i c1 <> mkLines i c2 834 | mkLines i (CIndent c) = mkLines (indentStr <> i) c 835 | mkLines i (CLine t) = [i <> t] 836 | indentStr = " " 837 | 838 | cline :: T.Text -> Code 839 | cline = CLine 840 | 841 | clines :: [T.Text] -> Code 842 | clines lines = mconcat (map CLine lines) 843 | 844 | cblank :: Code 845 | cblank = CLine "" 846 | 847 | ctemplate :: T.Text -> [T.Text] -> Code 848 | ctemplate pattern params = CLine $ T.template pattern params 849 | 850 | cgroup :: T.Text -> T.Text -> T.Text -> [T.Text] -> Code 851 | cgroup begin sep end [] = CLine (begin <> end) 852 | cgroup begin sep end (t0:ts) = CLine (begin <> t0) <> cgroup1 ts 853 | where 854 | cgroup1 [] = CLine end 855 | cgroup1 (t1:ts) = CLine (sep <> t1) <> cgroup1 ts 856 | 857 | enumCode :: T.Text -> T.Text -> [T.Text] -> Code 858 | enumCode htypename fieldprefix values 859 | = mconcat (intersperse cblank [decl,toResourceInstance]) 860 | where 861 | decl = ctemplate "data $1 = $2 deriving (Eq)" [htypename,T.intercalate " | " [hValue v | v <- values]] 862 | hValue v = fieldprefix <> "_" <> T.replace "-" "_" v 863 | toResourceInstance 864 | = ctemplate "instance ToResourceField $1 where" [htypename] 865 | <> mconcat [CIndent $ ctemplate " toResourceField $1 = \"$2\"" [hValue v,v] | v <- values] 866 | 867 | fieldsCode :: T.Text -> T.Text -> Bool -> [(T.Text, FieldType, FieldMode)] -> Code 868 | fieldsCode htypename fieldprefix deriveInstances args 869 | = mconcat (intersperse cblank [params,lenses,makeParams,toResourceInstance]) 870 | where 871 | params = 872 | ( ctemplate "data $1Params = $1Params" [htypename] 873 | <> CIndent (cgroup "{ " ", " "}" 874 | ( [T.template "_$1 :: $2" [hname fname,hftype ftype] | (fname,ftype,Required) <- args] 875 | <> 876 | [T.template "_$1 :: $2" [hname fname,optionalType ftype fmode] | (fname,ftype,fmode) <- args, isOptional fmode] 877 | ) 878 | <> if deriveInstances then cline "deriving (Eq)" else mempty 879 | ) 880 | ) 881 | lenses = mconcat [ 882 | ctemplate "-- $3 :: Lens' $1Params $2" [htypename,optionalType ftype optional,hname fname] 883 | <> ctemplate "$3 :: Functor f => ($2 -> f ($2)) -> $1Params -> f $1Params" [htypename,optionalType ftype optional,hname fname] 884 | <> ctemplate "$3 k atom = fmap (\\new$3 -> atom { _$3 = new$3 }) (k (_$3 atom))" [htypename,optionalType ftype optional,hname fname] 885 | | (fname,ftype,optional) <- args 886 | ] 887 | makeParams 888 | = ctemplate 889 | "make$1Params :: $2 $1Params" 890 | [ htypename 891 | , T.intercalate " " [hftype ftype <> " ->" | (_,ftype,Required) <- args] 892 | ] 893 | <> ctemplate 894 | "make$1Params $2 = $1Params" 895 | [ htypename 896 | , T.intercalate " " [hfnname fname | (fname,_,Required) <- args] 897 | ] 898 | <> CIndent (cgroup "{ " ", " "}" 899 | ( [T.template "_$1 = $2" [hname fname,hfnname fname] | (fname,ftype,Required) <- args] 900 | <> 901 | [T.template "_$1 = $2" [hname fname, optionalDefault fmode] | (fname,ftype,fmode) <- args, isOptional fmode] 902 | ) 903 | ) 904 | toResourceInstance 905 | = (ctemplate "instance ToResourceFieldMap $1Params where" [htypename]) 906 | <> CIndent 907 | (cline "toResourceFieldMap params" 908 | <> (CIndent (cgroup "= " "<> " "" (map createValue args))) 909 | ) 910 | <> cblank 911 | <> (ctemplate "instance ToResourceField $1Params where" [htypename]) 912 | <> CIndent 913 | (cline "toResourceField = RF_Map . toResourceFieldMap " 914 | ) 915 | 916 | createValue (fname,ftype,Required) = 917 | T.template "rfmField \"$1\" (_$2 params)" [dequote fname, hname fname] 918 | createValue (fname,ftype,Optional) = 919 | T.template "rfmOptionalField \"$1\" (_$2 params)" [dequote fname, hname fname] 920 | createValue (fname,ftype,OptionalWithDefault defv) = 921 | T.template "rfmOptionalDefField \"$1\" $2 (_$3 params)" [dequote fname, defv, hname fname] 922 | createValue (fname,ftype,ExpandedList) = 923 | T.template "rfmExpandedList \"$1\" (_$2 params)" [dequote fname, hname fname] 924 | 925 | dequote = T.takeWhile (/= '\'') 926 | 927 | hname n = fieldprefix <> "_" <> n 928 | 929 | resourceCode :: T.Text -> T.Text -> T.Text -> [(T.Text, FieldType, FieldMode)] -> [(T.Text, FieldType)] -> Code 930 | resourceCode tftypename fieldprefix docurl args attrs 931 | = mconcat (intersperse cblank [function,function',value,isResourceInstance,argsTypes]) 932 | where 933 | function 934 | = ctemplate "-- | Add a resource of type $1 to the resource graph." [htypename tftypename] 935 | <> cline "--" 936 | <> ctemplate "-- See the terraform <$1 $2> documentation" [docurl, tftypename] 937 | <> cline "-- for details." 938 | <> ctemplate "-- (In this binding attribute and argument names all have the prefix '$1_')" [fieldprefix] 939 | <> cline "" 940 | <> ctemplate 941 | "$1 :: NameElement -> $2($3Params -> $3Params) -> TF $3" 942 | [ hfnname tftypename 943 | , T.intercalate " " [hftype ftype <> " ->" | (_,ftype,Required) <- args] 944 | , htypename tftypename 945 | ] 946 | <> ctemplate 947 | "$1 name0 $2 modf = new$3 name0 (modf (make$3Params $2))" 948 | [ hfnname tftypename 949 | , T.intercalate " " [hfnname fname | (fname,_,Required) <- args] 950 | , htypename tftypename 951 | ] 952 | <> cline "" 953 | <> ctemplate 954 | "$1' :: NameElement -> $2 TF $3" 955 | [ hfnname tftypename 956 | , T.intercalate " " [hftype ftype <> " ->" | (_,ftype,Required) <- args] 957 | , htypename tftypename 958 | ] 959 | <> ctemplate 960 | "$1' name0 $2 = new$3 name0 (make$3Params $2)" 961 | [ hfnname tftypename 962 | , T.intercalate " " [hfnname fname | (fname,_,Required) <- args] 963 | , htypename tftypename 964 | ] 965 | 966 | function' 967 | = ctemplate "new$1 :: NameElement -> $1Params -> TF $1" [htypename tftypename] 968 | <> ctemplate "new$1 name0 params = do" [htypename tftypename] 969 | <> CIndent 970 | ( ctemplate "rid <- mkResource \"$1\" name0 (toResourceFieldMap params)" [tftypename] 971 | <> ctemplate "return $1" [htypename tftypename] 972 | <> CIndent (cgroup "{ " ", " "}" attrValues) 973 | ) 974 | 975 | attrValues 976 | = [T.template "$1 = resourceAttr rid \"$2\"" [hname fname, fname] | (fname,_) <- attrs] 977 | <> [T.template "$1_resource = rid" [fieldprefix]] 978 | 979 | argsTypes = fieldsCode (htypename tftypename) fieldprefix False args 980 | 981 | value = 982 | ( ctemplate "data $1 = $1" [htypename tftypename] 983 | <> CIndent (cgroup "{ " ", " "}" 984 | ( [T.template "$1 :: $2" [hname fname,hftype ftype] | (fname,ftype) <- attrs] 985 | <> [T.template "$1_resource :: ResourceId" [fieldprefix]] 986 | ) 987 | ) 988 | ) 989 | 990 | isResourceInstance = 991 | ctemplate "instance IsResource $1 where" [htypename tftypename] 992 | <> CIndent (ctemplate "resourceId = $1_resource" [fieldprefix]) 993 | 994 | hname n = fieldprefix <> "_" <> n 995 | 996 | hfnname tftype = unreserve (T.toLower c1 <> cs) 997 | where 998 | (c1,cs) = T.splitAt 1 (htypename tftype) 999 | unreserve n = if S.member n reserved then n <> "_" else n 1000 | reserved = S.fromList ["type","data","instance"] 1001 | 1002 | htypename tftype = T.concat (map T.toTitle (T.splitOn "_" tftype)) 1003 | 1004 | isOptional Optional = True 1005 | isOptional (OptionalWithDefault _) = True 1006 | isOptional ExpandedList = True 1007 | isOptional _ = False 1008 | 1009 | optionalType ftype Optional = T.template "Maybe ($1)" [hftype ftype] 1010 | optionalType ftype _ = hftype ftype 1011 | 1012 | optionalDefault Required = "??" 1013 | optionalDefault ExpandedList = "[]" 1014 | optionalDefault Optional = "Nothing" 1015 | optionalDefault (OptionalWithDefault def) = def 1016 | 1017 | hftype (NamedType t) = t 1018 | hftype (TFRef t) 1019 | | T.isInfixOf " " t = T.template "TFRef ($1)" [t] 1020 | | otherwise = T.template "TFRef $1" [t] 1021 | hftype (AwsIdRef t) = T.template "TFRef (AwsId $1)" [htypename t] 1022 | hftype (FTList t) = "[" <> hftype t <> "]" 1023 | hftype TagsMap = "M.Map T.Text T.Text" 1024 | 1025 | 1026 | generateModule :: FilePath -> T.Text -> Code -> [Code] -> IO () 1027 | generateModule outdir moduleName header resources = T.writeFile filepath (T.intercalate "\n" (codeText code)) 1028 | where 1029 | filepath = outdir (T.unpack moduleName <> ".hs") 1030 | 1031 | code = header0 <> cblank <> header <> csection <> mconcat (intersperse csection resources) 1032 | 1033 | csection = cblank <> cline (T.replicate 70 "-") <> cblank 1034 | header0 = clines 1035 | [ "{-# LANGUAGE OverloadedStrings #-}" 1036 | , "-- | Terraform resource definitions" 1037 | , "--" 1038 | , "-- This file is auto-generated. Change it by changing the script" 1039 | , "-- that generates it." 1040 | , "--" 1041 | , "-- There are two variants of each function to construct a resource" 1042 | , "-- (eg 'awsVpc' and 'awsVpc'') . The former takes the required attributes" 1043 | , "-- as positional paramemeters. The latter (with the quote suffixed name)" 1044 | , "-- takes a record containing all attributes. This can be more convenient" 1045 | , "-- when there are many required arguments." 1046 | , "--" 1047 | , T.template "module Language.Terraform.$1 where" [moduleName] 1048 | , "" 1049 | , "import qualified Data.Map as M" 1050 | , "import qualified Data.Text as T" 1051 | , "import Data.Maybe(catMaybes)" 1052 | , "import Data.Monoid" 1053 | , "import Language.Terraform.Core" 1054 | ] 1055 | 1056 | generate :: FilePath -> IO () 1057 | generate outdir = do 1058 | generateModule outdir "Aws" awsHeader awsResources 1059 | 1060 | 1061 | main :: IO () 1062 | main = generate "src/Language/Terraform" 1063 | -------------------------------------------------------------------------------- /src/Language/Terraform/Core.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings,FlexibleInstances, ScopedTypeVariables #-} 2 | -- | An EDSL for generating terraform code. 3 | -- 4 | -- The basic idea is that you compose an monadic `TF` action to specify 5 | -- the terraform resources and their dependencies, and then call `generateFiles` 6 | -- to actually generate the terraform files. An example: 7 | -- 8 | -- @ 9 | -- {-# LANGUAGE OverloadedStrings #-} 10 | -- import Language.Terraform.Core 11 | -- import Language.Terraform.Aws 12 | -- import Data.Default 13 | -- 14 | -- main = generateFiles "/tmp" $ do 15 | -- -- Construct an AWS virtual private cloud 16 | -- vpc <- awsVpc "vpc" "10.30.0.0/16" def 17 | -- -- Construct a route table 18 | -- awsRouteTable "rt" (vpc_id vpc) def 19 | -- return () 20 | -- @ 21 | -- 22 | -- In terraform, each resource requires a unique name. The name is 23 | -- provided as the first parameter to the function creating that 24 | -- resource. The `withNameScope` function helps composability by creating 25 | -- independent scopes for names. 26 | -- 27 | -- For more details on terraform, see 28 | -- https://www.terraform.io/intro/index.html 29 | 30 | module Language.Terraform.Core( 31 | ToResourceField(..), 32 | ToResourceFieldMap(..), 33 | IsResource(..), 34 | 35 | ResourceField(..), 36 | ResourceFieldMap(..), 37 | TF, 38 | TFRef(..), 39 | NameElement, 40 | Name, 41 | ResourceId, 42 | 43 | mkProvider, 44 | mkResource, 45 | output, 46 | resourceAttr, 47 | dependsOn, 48 | ignoreChanges, 49 | createBeforeDestroy, 50 | localExecProvisioner, 51 | withNameScope, 52 | scopedName, 53 | scopedName', 54 | nameContext, 55 | withContext, 56 | getContext, 57 | generateFiles, 58 | rfmField, 59 | rfmOptionalField, 60 | rfmOptionalDefField, 61 | rfmExpandedList, 62 | ) where 63 | 64 | import Data.Default 65 | import Data.Maybe(fromMaybe) 66 | import Data.Semigroup 67 | import Data.Monoid (Monoid (..)) 68 | import Data.Typeable 69 | import Data.Dynamic 70 | import Control.Applicative ((<$>)) 71 | import Control.Monad(void) 72 | import Control.Monad.Trans.State.Lazy(StateT,get,put,modify',runStateT) 73 | import System.FilePath(()) 74 | import Data.Foldable(for_) 75 | import Data.String(IsString(..)) 76 | 77 | import qualified Data.Map as M 78 | import qualified Data.Set as S 79 | import qualified Data.Text as T 80 | import qualified Data.Text.IO as T 81 | import qualified Language.Terraform.Util.Text as T 82 | 83 | type NameElement = T.Text 84 | 85 | -- | Every terraform resource has a unique name that is 86 | -- generated from it's name scope and the name provided 87 | -- to the resource constructor function. 88 | type Name = [NameElement] 89 | 90 | type TFType = T.Text 91 | 92 | data Provider = Provider { 93 | p_type :: T.Text, 94 | p_name :: [NameElement], 95 | p_fields :: ResourceFieldMap 96 | } 97 | 98 | data Resource = Resource { 99 | r_type :: T.Text, 100 | r_name :: [NameElement], 101 | r_fields :: ResourceFieldMap 102 | } 103 | 104 | data Output = Output { 105 | o_name :: [NameElement], 106 | o_value :: T.Text 107 | } 108 | 109 | data Provisioner = Provisioner { 110 | pv_type :: T.Text, 111 | pv_fields :: ResourceFieldMap 112 | } 113 | 114 | -- | A map to be embedded in the terraform output. 115 | -- 116 | -- we can't actually use a regular map here, as repeated 117 | -- keys are allowed. 118 | newtype ResourceFieldMap = ResourceFieldMap { unResourceFieldMap :: [(T.Text,ResourceField)] } 119 | 120 | instance Semigroup ResourceFieldMap where 121 | ResourceFieldMap f1 <> ResourceFieldMap f2 = ResourceFieldMap (f1 <> f2) 122 | 123 | instance Monoid ResourceFieldMap where 124 | mempty = ResourceFieldMap [] 125 | mappend = (<>) 126 | 127 | rfmField :: ToResourceField f => T.Text -> f -> ResourceFieldMap 128 | rfmField field v = ResourceFieldMap [(field, toResourceField v)] 129 | 130 | rfmOptionalField :: ToResourceField f => T.Text -> Maybe f -> ResourceFieldMap 131 | rfmOptionalField _ Nothing = mempty 132 | rfmOptionalField field (Just v) = rfmField field v 133 | 134 | rfmOptionalDefField :: (Eq f,ToResourceField f) => T.Text -> f -> f -> ResourceFieldMap 135 | rfmOptionalDefField field defv v | defv == v = mempty 136 | | otherwise = rfmField field v 137 | 138 | rfmExpandedList :: ToResourceField f => T.Text -> [f] -> ResourceFieldMap 139 | rfmExpandedList field vs = ResourceFieldMap [(field, toResourceField v) | v <- vs] 140 | 141 | -- | A value to be embedded in the terraform output. 142 | data ResourceField = RF_Text T.Text 143 | | RF_List [ResourceField] 144 | | RF_Map ResourceFieldMap 145 | 146 | 147 | -- | Typeclass for overloading the conversion of values to 148 | -- a `ResourceField` value. 149 | class ToResourceField a where 150 | toResourceField :: a -> ResourceField 151 | toResourceFieldList :: [a] -> ResourceField 152 | toResourceFieldList as = RF_List (map toResourceField as) 153 | 154 | -- | Typeclass for overloading the conversion of values to 155 | -- a `ResourceFieldMap` value. 156 | class ToResourceFieldMap a where 157 | toResourceFieldMap :: a -> ResourceFieldMap 158 | 159 | instance IsString ResourceField where 160 | fromString = RF_Text . T.pack 161 | 162 | -- | A unique identifier for a source. 163 | data ResourceId = ResourceId TFType Name 164 | deriving (Eq,Ord) 165 | 166 | -- | All terraform resources implement this class to support 167 | -- overloaded access to common features. 168 | class IsResource a where 169 | resourceId :: a -> ResourceId 170 | 171 | -- | `TFRef t` is a reference to a terraform derived value of type t. 172 | -- Such values typically depend on actually deploying the infrastructure 173 | -- before they become known. 174 | newtype TFRef t = TFRef { 175 | tfRefText :: T.Text 176 | } deriving (Eq) 177 | 178 | instance ToResourceField (TFRef t) where 179 | toResourceField (TFRef t) = RF_Text t 180 | 181 | instance ToResourceField Int where 182 | toResourceField v = RF_Text (T.pack (show v)) 183 | 184 | instance ToResourceField T.Text where 185 | toResourceField t = RF_Text t 186 | 187 | instance ToResourceField Char where 188 | toResourceField c = RF_Text (T.singleton c) 189 | toResourceFieldList cs = RF_Text (T.pack cs) 190 | 191 | instance ToResourceField Bool where 192 | toResourceField True = RF_Text "true" 193 | toResourceField False = RF_Text "false" 194 | 195 | instance ToResourceField a => ToResourceField [a] where 196 | toResourceField = toResourceFieldList 197 | 198 | instance ToResourceField a => ToResourceField (M.Map T.Text a) where 199 | toResourceField = RF_Map . ResourceFieldMap . M.toList . M.map toResourceField 200 | 201 | data ResourceLifeCycle = ResourceLifeCycle { 202 | rlc_ignoreChanges :: S.Set T.Text, 203 | rlc_createBeforeDestroy :: Bool 204 | } 205 | 206 | instance Default ResourceLifeCycle where 207 | def = ResourceLifeCycle S.empty False 208 | 209 | data TFState = TFState { 210 | tf_nameContext :: [NameElement], 211 | tf_context :: M.Map TypeRep Dynamic, 212 | tf_providers :: [Provider], 213 | tf_resources :: [Resource], 214 | tf_outputs :: [Output], 215 | tf_dependencies :: S.Set (ResourceId,ResourceId), 216 | tf_lifecycle :: M.Map ResourceId ResourceLifeCycle, 217 | tf_provisioners :: M.Map ResourceId [Provisioner] 218 | } 219 | 220 | -- | A state monad over IO that accumulates the 221 | -- terraform resource graph. 222 | type TF a = StateT TFState IO a 223 | 224 | nameText :: Name -> T.Text 225 | nameText nameElements = T.intercalate "_" (reverse nameElements) 226 | 227 | -- | Generate a global name based upon the the current scope. 228 | scopedName :: NameElement -> TF T.Text 229 | scopedName name0 = do 230 | context <- tf_nameContext <$> get 231 | return (nameText (name0:context)) 232 | 233 | -- | Generate a global name based upon the the current scope, returning 234 | -- the name components. 235 | scopedName' :: NameElement -> TF Name 236 | scopedName' name0 = do 237 | context <- tf_nameContext <$> get 238 | return (reverse (name0:context)) 239 | 240 | nameContext :: TF [NameElement] 241 | nameContext = tf_nameContext <$> get 242 | 243 | -- | Provide a more specific naming scope for the specified terraform 244 | -- action. 245 | withNameScope:: NameElement -> TF a -> TF a 246 | withNameScope name tfa = do 247 | s0 <- get 248 | put s0{tf_nameContext=name:tf_nameContext s0} 249 | a <- tfa 250 | s1 <- get 251 | put s1{tf_nameContext=tf_nameContext s0} 252 | return a 253 | 254 | withContext :: Typeable c => c -> TF a -> TF a 255 | withContext c tfa = do 256 | s0 <- get 257 | let dyn = toDyn c 258 | put s0{tf_context=M.insert (dynTypeRep dyn) dyn (tf_context s0)} 259 | a <- tfa 260 | s1 <- get 261 | put s1{tf_context=tf_context s0} 262 | return a 263 | 264 | getContext :: forall c . Typeable c => TF (Maybe c) 265 | getContext = do 266 | s0 <- get 267 | case M.lookup (typeRep (Proxy :: Proxy c)) (tf_context s0) of 268 | Nothing -> return Nothing 269 | (Just dyn) -> return (fromDynamic dyn) 270 | 271 | -- | Internal function for constructing terraform providers 272 | mkProvider :: TFType -> [(T.Text,ResourceField)] -> TF () 273 | mkProvider tftype fields = do 274 | name <- fmap tf_nameContext get 275 | let provider = Provider tftype name (ResourceFieldMap fields) 276 | modify' (\s -> s{tf_providers=provider:tf_providers s}) 277 | 278 | -- | Internal function for constructing terraform resources 279 | mkResource :: TFType -> NameElement -> ResourceFieldMap -> TF ResourceId 280 | mkResource tftype name0 fieldmap = do 281 | s <- get 282 | let name = name0:tf_nameContext s 283 | let resource = Resource tftype name fieldmap 284 | modify' (\s -> s{tf_resources=resource:tf_resources s}) 285 | return (ResourceId tftype name) 286 | 287 | -- | Internal function for constructing resource attributes 288 | resourceAttr :: ResourceId -> T.Text -> TFRef a 289 | resourceAttr (ResourceId tftype name) attr = TFRef (T.template "${$1.$2.$3}" [tftype, nameText name, attr]) 290 | 291 | -- | Add an output to the generated terraform. 292 | -- (See https://www.terraform.io/intro/getting-started/outputs.html) 293 | output :: NameElement -> T.Text -> TF () 294 | output name0 value = do 295 | s <- get 296 | let name = name0:tf_nameContext s 297 | let output = Output name value 298 | modify' (\s -> s{tf_outputs=output:tf_outputs s}) 299 | 300 | -- | Specifiy an explicit depedency betweeen resources. 301 | -- (See https://www.terraform.io/intro/getting-started/dependencies.html) 302 | dependsOn :: (IsResource r1,IsResource r2) => r1 -> r2 -> TF () 303 | dependsOn r1 r2 = modify' (\s->s{tf_dependencies=S.insert (resourceId r1, resourceId r2) (tf_dependencies s)}) 304 | 305 | -- | Specify that a resource will ignore changes to the specified attribute 306 | -- (See https://www.terraform.io/docs/configuration/resources.html#ignore_changes) 307 | ignoreChanges :: (IsResource r) => r -> T.Text -> TF () 308 | ignoreChanges r attr = modify' (\s->s{tf_lifecycle=M.alter (addIgnored attr) (resourceId r) (tf_lifecycle s)}) 309 | where 310 | addIgnored :: T.Text -> Maybe ResourceLifeCycle -> Maybe ResourceLifeCycle 311 | addIgnored field Nothing = addIgnored field (Just def) 312 | addIgnored field (Just rlc) = Just rlc{rlc_ignoreChanges=S.insert field (rlc_ignoreChanges rlc)} 313 | 314 | -- | Specify that a new resource will be created before an old one is destroyed. 315 | -- (See https://www.terraform.io/docs/configuration/resources.html#createBeforeDestroy) 316 | createBeforeDestroy :: (IsResource r) => r -> Bool -> TF () 317 | createBeforeDestroy r v = modify' (\s->s{tf_lifecycle=M.alter (setFlag v) (resourceId r) (tf_lifecycle s)}) 318 | where 319 | setFlag :: Bool -> Maybe ResourceLifeCycle -> Maybe ResourceLifeCycle 320 | setFlag v Nothing = setFlag v (Just def) 321 | setFlag v (Just rlc) = Just rlc{rlc_createBeforeDestroy=v} 322 | 323 | -- | Add a local command to run after a resource is provisioned 324 | localExecProvisioner :: IsResource r => r -> T.Text -> TF () 325 | localExecProvisioner r command = modify' (\s->s{tf_provisioners=M.insertWith (<>) (resourceId r) [provisioner] (tf_provisioners s)}) 326 | where 327 | provisioner = Provisioner "local-exec" (ResourceFieldMap [("command", (RF_Text command))]) 328 | 329 | -- | Execute the TF monadic action to generating the graph of terraform 330 | -- resources, writing them to the specified directory. 331 | generateFiles :: FilePath -> TF a -> IO a 332 | generateFiles outDir tfa = do 333 | (a,state) <- runStateT tfa state0 334 | let files = S.fromList [ file | (file:_) <- map (reverse.r_name) (tf_resources state)] 335 | `S.union` 336 | S.fromList [ file | (file:_) <- map (reverse.o_name) (tf_outputs state)] 337 | for_ files $ \file -> do 338 | let content 339 | = [generateProvider state p | p <- reverse (tf_providers state), matchName0 file (p_name p) ] 340 | <> [generateResource state r | r <- reverse (tf_resources state), matchName0 file (r_name r) ] 341 | <> [generateOutput r | r <- reverse (tf_outputs state), matchName0 file (o_name r) ] 342 | T.writeFile (outDir T.unpack file <> ".tf") (T.intercalate "\n\n" content) 343 | return a 344 | where 345 | state0 = TFState [] M.empty [] [] [] S.empty M.empty M.empty 346 | matchName0 n ns = case reverse ns of 347 | (n0:_) -> n == n0 348 | _ -> False 349 | 350 | generateProvider state p = T.intercalate "\n" ( 351 | [ T.template "provider \"$1\" {" [p_type p] ] 352 | <> 353 | generateFieldMap " " (p_fields p) 354 | <> 355 | ["}"] 356 | ) 357 | 358 | generateResource state r = T.intercalate "\n" ( 359 | [ T.template "resource \"$1\" \"$2\" {" [r_type r, nameText (r_name r)] ] 360 | <> 361 | generateFieldMap " " fieldMap 362 | <> 363 | mconcat [ [T.template " provisioner \"$1\" {" [pv_type pv]] 364 | <> 365 | generateFieldMap " " (pv_fields pv) 366 | <> 367 | [" }"] 368 | | pv <- provisioners ] 369 | 370 | <> 371 | lifecycle 372 | <> 373 | ["}"] 374 | ) 375 | where 376 | fieldMap = r_fields r <> dependsMap 377 | dependsMap | null depends = mempty 378 | | otherwise = ResourceFieldMap [("depends_on",(toResourceField [rtype <> "." <> nameText rname | (ResourceId rtype rname) <- depends]))] 379 | provisioners = fromMaybe [] (M.lookup rid (tf_provisioners state)) 380 | rid = ResourceId (r_type r) (r_name r) 381 | depends = [r2 | (r1,r2) <- S.toList (tf_dependencies state), r1 == rid] 382 | lifecycle = case M.lookup rid (tf_lifecycle state) of 383 | Nothing -> mempty 384 | (Just rlc) -> 385 | let ignoreChanges | S.null (rlc_ignoreChanges rlc) = mempty 386 | | otherwise = [T.template " ignore_changes = [$1]" 387 | [T.intercalate ", " [ "\"" <> attr <> "\"" | attr <- S.toList (rlc_ignoreChanges rlc)]]] 388 | createBeforeDestroy | rlc_createBeforeDestroy rlc = [" create_before_destroy = true"] 389 | | otherwise = mempty 390 | in case (ignoreChanges <> createBeforeDestroy) of 391 | [] -> mempty 392 | lines -> [" lifecycle {"] <> lines <> [" }"] 393 | 394 | generateFieldMap :: T.Text -> ResourceFieldMap -> [T.Text] 395 | generateFieldMap indent fieldMap = concatMap generateField (unResourceFieldMap fieldMap) 396 | where 397 | generateField (field,RF_Text value) = [T.template "$1$2 = $3" [indent,field,quotedText value]] 398 | generateField (field,RF_List values) 399 | = [T.template "$1$2 = [" [indent,field]] 400 | <> generateValues (indent <> " ") values 401 | <> [T.template "$1]" [indent]] 402 | generateField (field,RF_Map map) 403 | = [T.template "$1$2 {" [indent,field]] 404 | <> generateFieldMap (indent <> " ") map 405 | <> [T.template "$1}" [indent]] 406 | 407 | generateValues indent values = concatMap generateValue (zip values terms) 408 | where 409 | generateValue (RF_Text value,term) 410 | = [T.template "$1\"$2\"$3" [indent,value,term]] 411 | generateValue (RF_List values,term) 412 | = [indent <> "["] 413 | <> generateValues (indent <> " ") values 414 | <> [indent <> "]" <> term] 415 | generateValue (RF_Map map,term) 416 | = [indent <> "{"] 417 | <> generateFieldMap (indent <> " ") map 418 | <> [indent <> "}" <> term] 419 | 420 | terms = replicate (length values - 1) "," <> [""] 421 | 422 | generateOutput o = T.intercalate "\n" 423 | [ T.template "output \"$1\" {" [nameText (o_name o)] 424 | , T.template " value = \"$1\"" [o_value o] 425 | , "}" 426 | ] 427 | 428 | quotedText :: T.Text -> T.Text 429 | quotedText value 430 | | needsQuoting value = T.template "<<$1\n$2$3$1" 431 | [uniqueEof value, value, if T.isSuffixOf "\n" value then "" else "\n"] 432 | | otherwise = "\"" <> value <> "\"" 433 | 434 | needsQuoting :: T.Text -> Bool 435 | needsQuoting value = T.isInfixOf "\n" value || T.isInfixOf "\"" value 436 | 437 | uniqueEof :: T.Text -> T.Text 438 | uniqueEof value = head (filter (\eof -> not (T.isInfixOf eof value)) eofs) 439 | where 440 | eofs = ["EOF"] <> ["EOF" <> (T.pack (show n)) | n <- [1,2..]] 441 | 442 | -------------------------------------------------------------------------------- /src/Language/Terraform/Util/Text.hs: -------------------------------------------------------------------------------- 1 | -- | Helper functions for dealing with text values 2 | module Language.Terraform.Util.Text( 3 | template, 4 | show 5 | ) where 6 | 7 | import Prelude hiding(show) 8 | 9 | import qualified Prelude(show) 10 | import qualified Data.Text as T 11 | 12 | 13 | show :: (Show a) => a -> T.Text 14 | show = T.pack . Prelude.show 15 | 16 | -- | `template src substs` will replace all occurences the string $i 17 | -- in src with `substs !! i` 18 | template :: T.Text -> [T.Text] -> T.Text 19 | template t substs = foldr replace t (zip [1,2..] substs) 20 | where 21 | replace (i,s) t = T.replace (T.pack ('$':Prelude.show i)) s t 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | resolver: lts-10.10 3 | packages: 4 | - '.' 5 | -------------------------------------------------------------------------------- /terraform-hs.cabal: -------------------------------------------------------------------------------- 1 | name: terraform-hs 2 | version: 0.1.0.0 3 | synopsis: Initial project template from stack 4 | description: Please see README.md 5 | homepage: https://github.com/timbod7/terraform-hs#readme 6 | license: BSD3 7 | license-file: LICENSE 8 | author: Tim Docker 9 | maintainer: tim@dockerz.net 10 | copyright: 2017 Tim Docker 11 | category: Web 12 | build-type: Simple 13 | extra-source-files: README.md 14 | cabal-version: >=1.10 15 | 16 | library 17 | hs-source-dirs: src 18 | exposed-modules: Language.Terraform.Core 19 | , Language.Terraform.Aws 20 | , Language.Terraform.Util.Text 21 | build-depends: base >= 4.7 && < 5 22 | , containers > 0.5 && < 0.7 23 | , data-default >= 0.5 && < 0.8 24 | , filepath >= 1.4 && < 1.5 25 | , semigroups >= 0.16 && < 0.19 26 | , text >= 1.2 && < 1.3 27 | , transformers >= 0.4.2.0 && < 0.6 28 | default-language: Haskell2010 29 | 30 | source-repository head 31 | type: git 32 | location: https://github.com/timbod7/terraform-hs 33 | --------------------------------------------------------------------------------