diff --git a/build/default.nix b/build/default.nix index f3069f7..127edac 100644 --- a/build/default.nix +++ b/build/default.nix @@ -14,6 +14,8 @@ let platform-process platform-packaging platform-visual + platform-terraform + platform-deployment ]); in diff --git a/build/haskell.nix b/build/haskell.nix index e0c585d..0616ca6 100644 --- a/build/haskell.nix +++ b/build/haskell.nix @@ -16,6 +16,11 @@ rec { rev = "64e7bfb3abcad278e6160cd411abdd21a485a671"; }; + terraformHsSrc = fetchGit { + url = https://github.com/atidot/terraform-hs; + rev = "4815301f4d5cf8343907ea55e41f4f491930dc69"; + }; + platformTypesSrc = ../platform-types; platformDSLSrc = ../platform-dsl; platformAWSSrc = ../platform-aws; @@ -23,17 +28,22 @@ rec { platformProcessSrc = ../platform-process; platformPackagingSrc = ../platform-packaging; platformVisualSrc = ../platform-visual; + platformTerraformSrc = ../platform-terraform; + platformDeploymentSrc = ../platform-deployment; projectPackages = hspkgs: { - executor = ease hspkgs.executor; - stratosphere = hspkgs.callCabal2nix "stratosphere" "${stratosphereSrc}" {}; - platform-types = hspkgs.callCabal2nix "platform-types" "${platformTypesSrc}" {}; - platform-dsl = hspkgs.callCabal2nix "platform-dsl" "${platformDSLSrc}" {}; - platform-aws = hspkgs.callCabal2nix "platform-aws" "${platformAWSSrc}" {}; - platform-kube = hspkgs.callCabal2nix "platform-kube" "${platformKubeSrc}" {}; - platform-packaging = hspkgs.callCabal2nix "platform-packaging" "${platformPackagingSrc}" {}; - platform-process = hspkgs.callCabal2nix "platform-process" "${platformProcessSrc}" {}; - platform-visual = hspkgs.callCabal2nix "platform-visual" "${platformVisualSrc}" {}; + executor = ease hspkgs.executor; + stratosphere = hspkgs.callCabal2nix "stratosphere" "${stratosphereSrc}" {}; + terraform-hs = hspkgs.callCabal2nix "terraform-hs" "${terraformHsSrc}" {}; + platform-types = hspkgs.callCabal2nix "platform-types" "${platformTypesSrc}" {}; + platform-dsl = hspkgs.callCabal2nix "platform-dsl" "${platformDSLSrc}" {}; + platform-aws = hspkgs.callCabal2nix "platform-aws" "${platformAWSSrc}" {}; + platform-kube = hspkgs.callCabal2nix "platform-kube" "${platformKubeSrc}" {}; + platform-packaging = hspkgs.callCabal2nix "platform-packaging" "${platformPackagingSrc}" {}; + platform-process = hspkgs.callCabal2nix "platform-process" "${platformProcessSrc}" {}; + platform-visual = hspkgs.callCabal2nix "platform-visual" "${platformVisualSrc}" {}; + platform-terraform = hspkgs.callCabal2nix "platform-terraform" "${platformTerraformSrc}" {}; + platform-deployment = hspkgs.callCabal2nix "platform-deployment" "${platformDeploymentSrc}" {}; }; packages = haskellPackages.override (old: { diff --git a/platform-deployment/.gitignore b/platform-deployment/.gitignore new file mode 100644 index 0000000..997a3ad --- /dev/null +++ b/platform-deployment/.gitignore @@ -0,0 +1,3 @@ +.stack-work/ +platform-deployment.cabal +*~ \ No newline at end of file diff --git a/platform-deployment/LICENSE b/platform-deployment/LICENSE new file mode 100644 index 0000000..102126f --- /dev/null +++ b/platform-deployment/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2019 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/platform-deployment/README.md b/platform-deployment/README.md new file mode 100644 index 0000000..9bf9961 --- /dev/null +++ b/platform-deployment/README.md @@ -0,0 +1,77 @@ +# platform-deployment + +## Intro + +Allows us to declare a deployment inside an aws instance, with dockers, disks and secrets declarative management + +Platform deployment package holds two parts: + +1. Deployment-dsl - High language implemented with free monad, that describes the things we need in order to deploy software configuration (containers, secrets and data). [Here](src/Atidot/Platform/Deployment.hs) +1. Interpreters for deployment dsl - there are two interpreters +1. 1. AMI - starts a machine in aws with terraform configuration, then apply changes, and eventually, saves an AMI containing all the changes supplied. This is dynamic, which means that the machine state is changed after applying each line. [Here](src/Atidot/Platform/Deployment/Interpreter/AMI.hs) +1. 1. Terraform - Generates a terraform deployment file, that once applied, will start and then configure a full machine, on `terraform init`. This is static, which means that the interpreter is run, and then a file is generated. There is no AMI ready at the end of the interpreter run. It is more mature of the two. [here](https://github.com/Atidot/platform/blob/deployment/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Terraform.hs) + +## Terraform requirements and notes + +* [Terraform](https://www.terraform.io/) needs to be installed. +* In the terraform configuration, there is an sh key [path](src/Atidot/Platform/Deployment/Interpreter/AMI/Types/Default.hs#L23). It is needed to be supplied for the remote provisioners and for ssh connection into the deployed machine +* AWS credentials for configuring the aws cli are needed but are prompted during `terraform init` +* AWS EBS volumes are needed in order for mounting to work properly +* Volume is needed to be created before running the interpreter.It’s (Volume) name/id should be be added to the configuration [Here](src/Atidot/Platform/Deployment/Interpreter/Terraform/Template.hs#L26) +* Terraform deploys a single virtual machine with network components required for ssh connection into that machine. Then it uses a series of remote provisioners to change the state of the machine: +* 1. Installation of software and secrets initialization [Here](src/Atidot/Platform/Deployment/Interpreter/Terraform/Template.hs#L85) +* 1. secrets mounting code [Here](src/Atidot/Platform/Deployment/Interpreter/Terraform/Template.hs#L110) +* 1. Mounting and pulling dockers [Here](src/Atidot/Platform/Deployment/Interpreter/Terraform/Template.hs#L156) +* 1. Running dockers (Run command in the deployment DSL) [Here](src/Atidot/Platform/Deployment/Interpreter/Terraform/Template.hs#L189) +* In order to run new scripts, they should be added in [Main](app/Main.hs#L27) +Nes,nesa,std,sat,cdne - are error checking scripts, they should fail. +* All secrets used must be properly defined in advance in the AWS secrets manager + +## Next Steps + +* In order to avoid credentials for aws-cli completely, we can add an IAM role for the AMI [Here](https://dzone.com/articles/aws-secret-manager-protect-your-secrets-in-applica) +* CLI שrgument for terraform configurations (using json, from file?) +* Separate failed scripts (Nes,nesa,std,sat,cdne) to tests, to ensure that behavior is stable +* Instead of provisioner for running dockers, a provisioner for preparing a script that run dockers on computer start +* Better name giving to the ebs volumes (will be relevant if we want more than one volume) [Here](src/Atidot/Platform/Deployment/Interpreter/Terraform/Template.hs#L44) +* Improving the description of secrets in the dsl (they come in tuples at least, and are in a json format), + +## example script + +``` haskell +nsss :: DeploymentM Bool +nsss = do + secret <- secret "tutorials/MyFirstTutorialSecret" + dir <- mount "data" + c <- container "hello-world" + attachSecret secret c + attachVolume dir c + execute [] c [] + return b +``` + +This script Declares: + +1. A secret +2. An external disk "data" +3. a container named hello world +4. attaches the secret to the container +5. attaches the mount to the container +6. executes the container + +## How to Run + +``` bash +$ cd ../build +.. +$ make shell +... # in the newly opened nix shell +$ platform-deployment-exe --script nsss +{initializes terraform and generates files from example} +$ cd terraform_dep +... +$ ls +aws_cli_config.tf example.tf +$ terraform apply # applies genetated configuration +... +``` diff --git a/platform-deployment/app/Main.hs b/platform-deployment/app/Main.hs new file mode 100644 index 0000000..46f3890 --- /dev/null +++ b/platform-deployment/app/Main.hs @@ -0,0 +1,35 @@ +{-# LANGUAGE DeriveGeneric #-} +module Main where + +import "optparse-generic" Options.Generic +import "data-default" Data.Default +import Atidot.Platform.Deployment +import Atidot.Platform.Deployment.Interpreter.Terraform +import Atidot.Platform.Deployment.Interpreter.AMI.Types + +data CLI = CLI + {script :: String + } deriving (Generic, Show) + +instance ParseRecord CLI + + +main :: IO () +main = do + x <- script <$> getRecord "Platform Deployment" + case lookup x scripts of + Just s -> runTerraform def s + Nothing -> error $ unlines + [ "script '" ++ x ++ "' not found" + , "available scripts are: " ++ show (map fst scripts) + ] + +scripts = + [ ("nsss",nsss) + , ("kiss",kiss) + , ("nes",noneExistentSecret) + , ("nesa",noneExistentSecretAttached) + , ("sdt", secretDeclaredTwice) + , ("sat",secretAttachedTwice) + , ("cdne",containerDoesNotExists) + ] \ No newline at end of file diff --git a/platform-deployment/platform-deployment.cabal b/platform-deployment/platform-deployment.cabal new file mode 100644 index 0000000..e9a0f1e --- /dev/null +++ b/platform-deployment/platform-deployment.cabal @@ -0,0 +1,114 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.31.0. +-- +-- see: https://github.com/sol/hpack +-- +-- hash: 61e356af67999cee1223314d87fdfdd592f960368b933db1b8f345df60667e82 + +name: platform-deployment +version: 0.1.0.0 +description: Please see the README on GitHub at +homepage: https://github.com/githubuser/platform-deployment#readme +bug-reports: https://github.com/githubuser/platform-deployment/issues +author: Author name here +maintainer: example@example.com +copyright: 2019 Author name here +license: BSD3 +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + ChangeLog.md + +source-repository head + type: git + location: https://github.com/githubuser/platform-deployment + +library + exposed-modules: + Atidot.Platform.Deployment + Atidot.Platform.Deployment.Interpreter.AMI + Atidot.Platform.Deployment.Interpreter.AMI.Template + Atidot.Platform.Deployment.Interpreter.AMI.Types + Atidot.Platform.Deployment.Interpreter.AMI.Types.Default + Atidot.Platform.Deployment.Interpreter.AMI.Types.Types + Atidot.Platform.Deployment.Interpreter.Terraform + Atidot.Platform.Deployment.Interpreter.Terraform.Template + Atidot.Platform.Deployment.Interpreter.Test + Atidot.Platform.Deployment.Interpreter.Utils + other-modules: + Paths_platform_deployment + hs-source-dirs: + src + default-extensions: PackageImports OverloadedStrings ScopedTypeVariables + ghc-options: -Wall -Werror + build-depends: + aeson + , base >=4.7 && <5 + , containers + , data-default + , directory + , exceptions + , free + , ginger + , mtl + , optparse-generic + , raw-strings-qq + , text + , turtle + , uuid + default-language: Haskell2010 + +executable platform-deployment-exe + main-is: Main.hs + other-modules: + Paths_platform_deployment + hs-source-dirs: + app + default-extensions: PackageImports OverloadedStrings ScopedTypeVariables + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: + aeson + , base >=4.7 && <5 + , containers + , data-default + , directory + , exceptions + , free + , ginger + , mtl + , optparse-generic + , platform-deployment + , raw-strings-qq + , text + , turtle + , uuid + default-language: Haskell2010 + +test-suite platform-deployment-test + type: exitcode-stdio-1.0 + main-is: Spec.hs + other-modules: + Paths_platform_deployment + hs-source-dirs: + test + default-extensions: PackageImports OverloadedStrings ScopedTypeVariables + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: + aeson + , base >=4.7 && <5 + , containers + , data-default + , directory + , exceptions + , free + , ginger + , mtl + , optparse-generic + , platform-deployment + , raw-strings-qq + , text + , turtle + , uuid + default-language: Haskell2010 diff --git a/platform-deployment/src/Atidot/Platform/Deployment.hs b/platform-deployment/src/Atidot/Platform/Deployment.hs new file mode 100644 index 0000000..a8ea355 --- /dev/null +++ b/platform-deployment/src/Atidot/Platform/Deployment.hs @@ -0,0 +1,100 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE FlexibleContexts #-} + +module Atidot.Platform.Deployment where + +import "base" Data.Typeable +import "free" Control.Monad.Free +import "free" Control.Monad.Free.TH +import "text" Data.Text(Text) + +data Volume = Volume FilePath +data Disk = Disk FilePath + + +type Name = Text +type SecretName = Text +type DiskName = Text +type VolumeName = Text +type FolderDir = Text +type Arg = Text + +data Deployment a + -- resource declarations + = Container Name (Name -> a) -- bool + | Secret SecretName (SecretName -> a) + | Mount DiskName (VolumeName -> a) + -- resource attachments + | AttachSecret SecretName Name a + | AttachVolume FolderDir Name a + -- execution + | Execute [Arg] Name [Arg] a + + + deriving (Typeable, Functor) + +type DeploymentM = Free Deployment + +makeFree ''Deployment + +placeHolderContainer :: Name +placeHolderContainer = "helloWorld" + +placeHolderSecret :: SecretName +placeHolderSecret = "tutorials/MyFirstTutorialSecret" -- this must exist in aws secrets manager in the user's account + +placeHolderData :: DiskName +placeHolderData = "data" + +hello :: DeploymentM () +hello = do + _ <- container placeHolderContainer + return () + +nsss :: DeploymentM () +nsss = do + s <- secret placeHolderSecret -- declares secret that already exists in aws secrets manager + dir <- mount placeHolderData -- declares the mounting of volume data into the machine + c <- container placeHolderContainer -- declares the container running hello world + attachSecret s c -- attaches secret to the container + attachVolume dir c -- attaches the volume to the container also + execute [] c [] -- executes the program inside the container + + +kiss :: DeploymentM () +kiss = do + _dbUrl <- secret placeHolderSecret + _volume1 <- mount placeHolderData + c <- container placeHolderContainer + execute [] c [] + +noneExistentSecret :: DeploymentM () +noneExistentSecret = do + let noneExistantSecretPlaceholder = "some/secret" + _ <- secret noneExistantSecretPlaceholder + return () + +noneExistentSecretAttached :: DeploymentM () +noneExistentSecretAttached = do + let noneExistantSecretPlaceholder = "some/secret" + attachSecret noneExistantSecretPlaceholder placeHolderContainer + return () + +secretDeclaredTwice :: DeploymentM () +secretDeclaredTwice = do + _ <- secret placeHolderSecret + _ <- secret placeHolderSecret + return () + +secretAttachedTwice :: DeploymentM () +secretAttachedTwice = do + s <- secret placeHolderSecret + c <- container placeHolderContainer + attachSecret s c + attachSecret s c + +containerDoesNotExists :: DeploymentM () +containerDoesNotExists = do + s <- secret placeHolderSecret + attachSecret s placeHolderContainer diff --git a/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI.hs b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI.hs new file mode 100644 index 0000000..9efccda --- /dev/null +++ b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI.hs @@ -0,0 +1,116 @@ +module Atidot.Platform.Deployment.Interpreter.AMI where + +import Prelude hiding (FilePath) +import "text" Data.Text (Text) +import qualified "text" Data.Text as T +import "free" Control.Monad.Free +import "mtl" Control.Monad.State +import "exceptions" Control.Monad.Catch (bracket) +import "turtle" Turtle +import "uuid" Data.UUID.V4 (nextRandom) +import "directory" System.Directory (doesFileExist) + +import Atidot.Platform.Deployment.Interpreter.Utils +import Atidot.Platform.Deployment +import Atidot.Platform.Deployment.Interpreter.AMI.Template +import Atidot.Platform.Deployment.Interpreter.AMI.Types + +-- // to login +-- // $ ssh ubuntu@ -i ~/.ssh/terraform-keys2 + + +runAMI :: AMIConfig + -> DeploymentM a + -> IO () +runAMI config dep = + bracket init' + fini + body + where + terraformAwsDep = renderProvider (_AMIConfig_terraformConfig config) allTemplates + init' :: IO Text + init' = do + mktree terraformDepDir + cd terraformDepDir + output "example.tf" $ select $ textToLines terraformAwsDep + procs "terraform" ["init"] stdin + procs "terraform" ["apply", "-auto-approve"] stdin + showOutput <- reduceShell $ inproc "terraform" ["show"] stdin + sleep 8 -- wait for ssh on the remote machine to establish + let publicDns = getPublicDns showOutput + sshW publicDns ["sudo","apt","update"] + sshW publicDns ["sudo","apt","install","docker.io","-y"] + sshW publicDns ["sudo","usermod","-aG","docker","$USER"] + sshW publicDns ["mkdir",rSecretsDir] + return publicDns + fini :: Text -> IO () + fini _publicDns = do + -- save ami + --showOutput <- reduceShell $ inproc "terraform" ["show"] stdin + --let instanceId = getInstanceId showOutput + --amiName = instanceId <> "-ami" + --procs "aws" ["ec2", "create-image", "--instance-id", instanceId, "--name", amiName] stdin + + -- destroy all resources + procs "terraform" ["destroy"] stdin + cd ".." + return () + body :: Text -> IO () + body publicDns = do + _ <- (runStateT (iterM (run publicDns) dep) config) + return () + + run :: Text + -> Deployment (StateT AMIConfig IO a) + -> StateT AMIConfig IO a + run publicDns (Container containerName next) = do + conf <- get + let diskMappings = filter ((== Just containerName) . snd . snd ) $ _AMIConfig_mounts conf + diskMappingsInDocker = concatMap (\(vol,(disk,_)) -> ["-v",vol <> ":" <> disk]) diskMappings + lift $ sshW publicDns $ ["docker","run"] <> diskMappingsInDocker <> [containerName] + next containerName + + run publicDns (Secret secretData next) = do + isFile <- liftIO $ doesFileExist $ T.unpack secretData + if isFile then do + -- path <- copy file into remote location + -- conf <- get + nuid <- liftIO nextRandom + scpW publicDns secretData rSecretsDir + --let fname = encodeString $ fromText rSecretsDir filename (decodeString secretData) + -- add path to state + -- conf' = conf{ _AMIConfig_secrets = _AMIConfig_secrets conf <> [(nuid,(secretData,Just fname,Nothing))]} + next $ T.pack $ show nuid + else do + conf <- get + nuid <- liftIO nextRandom + -- store directly in the st ate + let conf' = conf{ _AMIConfig_secrets = _AMIConfig_secrets conf <> [(nuid,(T.unpack secretData,Nothing,Nothing))]} + put conf' + next $ T.pack $ show nuid + + run _ (Mount disk next) = do + conf <- get + nuid <- liftIO nextRandom + let volume = T.pack $ "disk-" <> show nuid + let containerName = case lookup volume $ _AMIConfig_mounts conf of + Just (_,cont) -> cont + Nothing -> Nothing + newMounts = (<> [(volume,(disk,containerName))]) $ filter ((/= volume) . fst) $ _AMIConfig_mounts conf + put $ conf{_AMIConfig_mounts = newMounts} + next volume + run _ (AttachSecret _ _ next) = next + run _ (AttachVolume _ _ next) = next + run _ (Execute _ _ _ next) = next + +sshW :: Text + -> [Text] + -> IO () +sshW publicDns cmd = procs "ssh" (["ubuntu@"<>publicDns,"-o","StrictHostKeyChecking=no","-i","~/.ssh/terraform-keys2"] <> cmd) stdin + +scpW :: MonadIO io + => Text + -> Text + -> Text + -> io () +scpW publicDns lcl rmt = procs "scp" ["-o","StrictHostKeyChecking=no","-i","~/.ssh/terraform-keys2",lcl, "ubuntu" <> "@" <> publicDns <> ":" <> rmt] stdin diff --git a/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Template.hs b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Template.hs new file mode 100644 index 0000000..f594691 --- /dev/null +++ b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Template.hs @@ -0,0 +1,177 @@ +{-# LANGUAGE QuasiQuotes #-} +module Atidot.Platform.Deployment.Interpreter.AMI.Template where + +import "text" Data.Text (Text) +import "ginger" Text.Ginger +import "raw-strings-qq" Text.RawString.QQ +import "mtl" Control.Monad.Writer (Writer) +import "mtl" Control.Monad.Identity (Identity(..)) +import Atidot.Platform.Deployment.Interpreter.AMI.Types + +nullResolver :: IncludeResolver Identity +nullResolver = const $ return Nothing + +toTemplate :: String -> Template SourcePos +toTemplate template = either (error . show) id . runIdentity $ + parseGinger nullResolver Nothing template + +renderProvider :: TerraformConfig + -> String + -> Text +renderProvider config template = do + let ctx :: GVal (Run SourcePos (Writer Text) Text) + ctx = toCtx config + easyRender ctx $ toTemplate template + where + toCtx :: TerraformConfig -> GVal (Run SourcePos (Writer Text) Text) + toCtx conf = dict $ map (\(a,b) -> a ~> b conf) + [ ("region" , _TerraformConfig_region ) + , ("profile" , _TerraformConfig_profile ) + , ("vpcName" , _TerraformConfig_vpcName ) + , ("gatewayName" , _TerraformConfig_gatewayName ) + , ("subnetName" , _TerraformConfig_subnetName ) + , ("routeTableName" , _TerraformConfig_routeTableName ) + , ("routeTableAssocName" , _TerraformConfig_routeTableAssocName ) + , ("securityGroupName" , _TerraformConfig_securityGroupName ) + , ("instanceName" , _TerraformConfig_instanceName ) + , ("eipName" , _TerraformConfig_eipName ) + , ("keyName" , _TerraformConfig_keyName ) + , ("s3BucketName" , _TerraformConfig_s3BucketName ) + , ("ebsVolumeName" , _TerraformConfig_ebs_volume ) + ] + +allTemplates :: String +allTemplates = foldl1 (<>) + [ provider + , awsVpc + , awsInternetGateway + , awsSubnet + , awsRouteTable + , awsRouteTableAssoc + , awsSecurityGroup + , awsInstance + , awsEip + , awsKeyPair + ] + +provider :: String +provider = [r| +provider "aws" { + version = "~> 2.39" + region = "{{region}}" + profile = "{{profile}}" +} + |] + +awsVpc :: String +awsVpc = [r| +resource "aws_vpc" "{{vpcName}}" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true +} + |] + +awsInternetGateway :: String +awsInternetGateway = [r| +resource "aws_internet_gateway" "{{gatewayName}}" { + vpc_id = aws_vpc.{{vpcName}}.id +} + |] + +awsSubnet :: String +awsSubnet = [r| +resource "aws_subnet" "{{subnetName}}" { + vpc_id = aws_vpc.{{vpcName}}.id + cidr_block = cidrsubnet(aws_vpc.{{vpcName}}.cidr_block, 3, 1) + availability_zone = "{{region}}a" +} + |] + +awsRouteTable :: String +awsRouteTable = [r| +resource "aws_route_table" "{{routeTableName}}" { + vpc_id = aws_vpc.{{vpcName}}.id + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.{{gatewayName}}.id + } +} + |] + +awsRouteTableAssoc :: String +awsRouteTableAssoc = [r| +resource "aws_route_table_association" "{{routeTableAssocName}}" { + subnet_id = aws_subnet.{{subnetName}}.id + route_table_id = aws_route_table.{{routeTableName}}.id +} + |] + +awsSecurityGroup :: String +awsSecurityGroup = [r| +resource "aws_security_group" "{{securityGroupName}}" { + ingress { + from_port = "22" + to_port = "22" + protocol = "tcp" + cidr_blocks = [ + "0.0.0.0/0" + ] + } + egress { + from_port = "0" + to_port = "0" + protocol = "-1" + cidr_blocks = [ + "0.0.0.0/0" + ] + } + vpc_id = aws_vpc.{{vpcName}}.id +} + |] + +awsInstance :: String +awsInstance = [r| +resource "aws_instance" "{{instanceName}}" { + ami = "ami-2757f631" + instance_type = "t2.micro" + subnet_id = aws_subnet.{{subnetName}}.id + key_name = "{{keyName}}" + vpc_security_group_ids = [ + aws_security_group.{{securityGroupName}}.id + ] +} + |] + +awsEip :: String +awsEip = [r| +resource "aws_eip" "{{eipName}}" { + vpc = "true" + instance = aws_instance.{{instanceName}}.id +} + |] + +awsKeyPair :: String +awsKeyPair = [r| +resource "aws_key_pair" "{{keyName}}" { + key_name = "{{keyName}}" + public_key = file("~/.ssh/{{keyName}}.pub") +} + |] + +awsS3Bucket :: String +awsS3Bucket = [r| +resource "aws_s3_bucket" "{{s3BucketName}}" { + bucket = "atidot-tf-test-bucket" + acl = "private" +} + |] + +awsEbsVolume :: String +awsEbsVolume = [r| +resource "aws_volume_attachment" "ebs_att" { + device_name = "/dev/sdg" + volume_id = "vol-0d2df0f8a79885bc3" + instance_id = aws_instance.atidot-micro-instance.id + skip_destroy = true +} + |] diff --git a/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Types.hs b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Types.hs new file mode 100644 index 0000000..9276bd9 --- /dev/null +++ b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Types.hs @@ -0,0 +1,6 @@ +module Atidot.Platform.Deployment.Interpreter.AMI.Types + ( module Atidot.Platform.Deployment.Interpreter.AMI.Types.Types + ) where + +import Atidot.Platform.Deployment.Interpreter.AMI.Types.Types +import Atidot.Platform.Deployment.Interpreter.AMI.Types.Default () diff --git a/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Types/Default.hs b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Types/Default.hs new file mode 100644 index 0000000..89ac70e --- /dev/null +++ b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Types/Default.hs @@ -0,0 +1,25 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Atidot.Platform.Deployment.Interpreter.AMI.Types.Default where + +import "data-default" Data.Default +import Atidot.Platform.Deployment.Interpreter.AMI.Types.Types + +instance Default AMIConfig where + def = AMIConfig [] [] [] def + +instance Default TerraformConfig where + def = TerraformConfig + { _TerraformConfig_region = "us-east-1" + , _TerraformConfig_profile = "default" + , _TerraformConfig_vpcName = "atidot-vpc" + , _TerraformConfig_gatewayName = "atidot-gw" + , _TerraformConfig_subnetName = "atidot-subnet" + , _TerraformConfig_routeTableName = "atidot-route-table" + , _TerraformConfig_routeTableAssocName = "atidot-route-table-assoc" + , _TerraformConfig_securityGroupName = "atidot-security-group" + , _TerraformConfig_instanceName = "atidot-micro-instance" + , _TerraformConfig_eipName = "atidot-eip" + , _TerraformConfig_keyName = "terraform-keys2" + , _TerraformConfig_s3BucketName = "atidot-s3-bucket" + , _TerraformConfig_ebs_volume = "atidot_ebs_vol_1" + } diff --git a/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Types/Types.hs b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Types/Types.hs new file mode 100644 index 0000000..3374a26 --- /dev/null +++ b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/AMI/Types/Types.hs @@ -0,0 +1,43 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DeriveDataTypeable #-} +module Atidot.Platform.Deployment.Interpreter.AMI.Types.Types where + +import "base" Data.Data (Data) +import "base" Data.Typeable (Typeable) +import "base" GHC.Generics (Generic) +import "text" Data.Text (Text) +import "uuid" Data.UUID + +type ContainerName = Text +type DiskName = Text +type VolumeName = Text +type SecretName = UUID +type SecretData = String +type SecretAsMount = FilePath -- for the case that the secret is a file + +data AMIConfig = + AMIConfig + { _AMIConfig_secrets :: [(SecretName,(SecretData,Maybe SecretAsMount,Maybe ContainerName))] + , _AMIConfig_configs :: [(String,Maybe String)] + , _AMIConfig_mounts :: [(VolumeName,(DiskName,Maybe ContainerName))] + , _AMIConfig_terraformConfig :: TerraformConfig + } + deriving (Show, Read, Eq, Ord, Data, Typeable, Generic) + +data TerraformConfig = + TerraformConfig + { _TerraformConfig_region :: !Text + , _TerraformConfig_profile :: !Text + , _TerraformConfig_vpcName :: !Text + , _TerraformConfig_gatewayName :: !Text + , _TerraformConfig_subnetName :: !Text + , _TerraformConfig_routeTableName :: !Text + , _TerraformConfig_routeTableAssocName :: !Text + , _TerraformConfig_securityGroupName :: !Text + , _TerraformConfig_instanceName :: !Text + , _TerraformConfig_eipName :: !Text + , _TerraformConfig_keyName :: !Text + , _TerraformConfig_s3BucketName :: !Text + , _TerraformConfig_ebs_volume :: !Text + } + deriving (Show, Read, Eq, Ord, Data, Typeable, Generic) diff --git a/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Terraform.hs b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Terraform.hs new file mode 100644 index 0000000..0ed6616 --- /dev/null +++ b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Terraform.hs @@ -0,0 +1,167 @@ +{-# LANGUAGE FlexibleContexts #-} +module Atidot.Platform.Deployment.Interpreter.Terraform where + +import qualified "text" Data.Text as T +import "base" Data.Maybe +import "exceptions" Control.Monad.Catch --(catch, bracket) +import "free" Control.Monad.Free +import "mtl" Control.Monad.State +import "turtle" Turtle hiding (x, s, d) +import qualified "containers" Data.Map as M + +import Atidot.Platform.Deployment.Interpreter.Utils +import Atidot.Platform.Deployment.Interpreter.Terraform.Template +import Atidot.Platform.Deployment hiding (VolumeName, SecretName, Name, FolderDir) + + +runTerraform :: TerraformExtendedConfig + -> DeploymentM a + -> IO () +runTerraform config dep = + bracket init' + fini + body + where + init' = do + mktree terraformDepDir + cd terraformDepDir + return () + body _ = do + (_,s) <- runStateT (iterM run dep) config + output "example.tf" $ select $ textToLines $ renderTerraform s + output "aws_cli_config.tf" $ select $ textToLines awsConfigVars + procs "terraform" ["init"] stdin + return () + fini _ = return () + + run :: Deployment (StateT TerraformExtendedConfig IO a) + -> StateT TerraformExtendedConfig IO a + run (Container containerName next) = do + updateDockers $ T.unpack containerName + updatePrep ["docker","pull", T.unpack containerName] + next containerName + run (Secret secretName next) = do + -- pull secrets from aws vault + secretRetrivalFailed secretName $ shells ("aws secretsmanager get-secret-value --secret-id " <> secretName <> " > /dev/null 2>&1") stdin + addSecret $ T.unpack secretName + next $ T.pack $ secretifyName $ T.unpack secretName + run (Mount folderName next) = do + (devMapping, volId) <- getNextDisk + addDisk devMapping volId + let folderDir = "/" <> T.unpack folderName + updatePrep ["sudo","mkdir","-p",folderDir] + updatePrep ["sudo","mount", "/dev/" <> devMapping, folderDir] + updatePrep ["echo", "/dev/" <> devMapping, folderDir, "xfs", "defaults,nofail", "0", "2", "|", "sudo", "tee", "-a", "/etc/fstab"] + updatePrep ["sudo","cat","/etc/fstab"] + next $ T.pack folderDir + run (AttachSecret secretName name next) = do + conf <- get + let containerMapping = case M.lookup (T.unpack name) (_TerraformExtendedConfig_dockers conf) of + Just x -> x + Nothing -> error $ "container '" <> T.unpack name <> "' does not exist" + secretsForAttachment = fst containerMapping + secrets = _TerraformExtendedConfig_secrets conf + secretName' = T.unpack secretName + unless ( secretName' `elem` map secretifyName secrets) $ error $ "secret '" <> secretName' <> "' not found for attachment" + when ( secretName' `elem` secretsForAttachment) $ error $ "secret '" <> secretName' <> "' is already attached to container '" <> T.unpack name <> "'" + attachDockerSecret (T.unpack name) secretName' + next + run (AttachVolume folderDir name next) = do + attachDockerFolder (T.unpack name) (T.unpack folderDir) + next + run (Execute containerEngineArgs name containerArgs next) = do + conf <- get + let name' = T.unpack name + (secrets,dirs) = fromMaybe ([],[]) $ M.lookup name' $ _TerraformExtendedConfig_dockers conf + dirs' = map (\d -> ["-v",d <> ":" <> d]) dirs + secrets' = map (\s -> ["-e",s <> "=$" <> s]) secrets + cmd = ["docker","run"] ++ map T.unpack containerEngineArgs ++ concat dirs' ++ concat secrets' ++ [name'] ++ map T.unpack containerArgs + updateExec cmd + next + +updatePrep :: (MonadState TerraformExtendedConfig m, Foldable t) + => t [Char] + -> m () +updatePrep cmd = modify $ \s -> s{ _TerraformExtendedConfig_instancePrep = _TerraformExtendedConfig_instancePrep s <> addCmd cmd} + +updateExec :: (MonadState TerraformExtendedConfig m, Foldable t) + => t [Char] + -> m () +updateExec cmd = modify $ \s -> s{ _TerraformExtendedConfig_instanceExec = _TerraformExtendedConfig_instanceExec s <> addCmd cmd} + +addCmd :: (Foldable t, Semigroup a, IsString a) + => t a + -> [a] +addCmd cmd = [foldl1 (\x y -> x <> " " <> y) cmd] + +attachDockerFolder :: MonadState TerraformExtendedConfig m + => Name + -> FolderDir + -> m () +attachDockerFolder name folderDir = modify $ \s -> case M.lookup name (_TerraformExtendedConfig_dockers s) of + Nothing -> error $ "attachDockerFolder: docker '" ++ show name ++ "' not found" + Just _ -> s{ _TerraformExtendedConfig_dockers = + M.insertWith + (\a b -> (fst a ++ fst b,snd a ++ snd b)) + name + ([],[folderDir]) + (_TerraformExtendedConfig_dockers s) + } + +attachDockerSecret :: MonadState TerraformExtendedConfig m + => Name + -> SecretName + -> m () +attachDockerSecret name sec = modify $ \s -> case M.lookup name (_TerraformExtendedConfig_dockers s) of + Nothing -> error $ "attachDockerSecret: docker '" ++ show name ++ "' not found" + Just _ -> s{ _TerraformExtendedConfig_dockers = + M.insertWith + (\a b -> (fst a ++ fst b,snd a ++ snd b)) + name + ([sec],[]) + (_TerraformExtendedConfig_dockers s) + } + +updateDockers :: MonadState TerraformExtendedConfig m + => Name + -> m () +updateDockers name = modify $ \s -> if M.member name (_TerraformExtendedConfig_dockers s) + then s + else s{ _TerraformExtendedConfig_dockers = + M.insert + name + ([],[]) + (_TerraformExtendedConfig_dockers s) + } + +addDisk :: MonadState TerraformExtendedConfig m + => DeviceName + -> VolumeName + -> m () +addDisk diskName volume = modify $ \s -> s{ _TerraformExtendedConfig_disks = _TerraformExtendedConfig_disks s <> [(diskName,volume)]} + + +addSecret :: MonadState TerraformExtendedConfig m + => SecretName + -> m () +addSecret secretName = modify $ \s -> if elem secretName $ _TerraformExtendedConfig_secrets s + then error $ "secret '" <> secretName <> "' already exists" + else s{ _TerraformExtendedConfig_secrets = _TerraformExtendedConfig_secrets s <> [secretName]} + +getNextDisk :: StateT TerraformExtendedConfig IO (DeviceName, VolumeName) +getNextDisk = do + conf <- get + let disks = _TerraformExtendedConfig_availableDisks conf + if null disks + then error "no more disks to attach" + else do + let (physDisk:rest) = disks + put $ conf{ _TerraformExtendedConfig_availableDisks = rest} + return physDisk + + +secretRetrivalFailed :: MonadCatch m + => Text + -> m a + -> m a +secretRetrivalFailed secretName action = action `catch` (\(_ :: ShellFailed) -> error $ "secret '" <> T.unpack secretName <> "' not found") \ No newline at end of file diff --git a/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Terraform/Template.hs b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Terraform/Template.hs new file mode 100644 index 0000000..038ea73 --- /dev/null +++ b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Terraform/Template.hs @@ -0,0 +1,294 @@ +{-# LANGUAGE QuasiQuotes #-} +module Atidot.Platform.Deployment.Interpreter.Terraform.Template where + +import "text" Data.Text (Text) +import "base" Data.Char +import "ginger" Text.Ginger +import "raw-strings-qq" Text.RawString.QQ +import "mtl" Control.Monad.Writer (Writer) +import "data-default" Data.Default +import qualified "containers" Data.Map as M +import qualified "text" Data.Text as T + +import Atidot.Platform.Deployment.Interpreter.AMI.Types hiding (DiskName,SecretName,VolumeName) +import Atidot.Platform.Deployment.Interpreter.AMI.Template hiding (awsInstance,allTemplates, awsEbsVolume) + +placeHolderAwsEbsVolumes :: [VolumeName] +placeHolderAwsEbsVolumes = ["vol-01ac704e80ba48949"] -- These should be created in the AWS before running the code +placeHoldersPhysicalDiskMappings :: [DeviceName] +placeHoldersPhysicalDiskMappings = ["xvdh","sdf","sdg","sdh","sdj"] + +instance Default TerraformExtendedConfig where + def = TerraformExtendedConfig + [] + [] + [] + [] + M.empty + def + $ zip + placeHoldersPhysicalDiskMappings + placeHolderAwsEbsVolumes + +type Cmd = String +type SecretName = String +type DiskName = String +type VolumeName = String -- ebs volume name +type Name = String +type DeviceName = String +type FolderDir = String + + +data TerraformExtendedConfig = TerraformExtendedConfig + { _TerraformExtendedConfig_instancePrep :: [Cmd] + , _TerraformExtendedConfig_instanceExec :: [Cmd] + , _TerraformExtendedConfig_disks :: [(DeviceName,VolumeName)] + , _TerraformExtendedConfig_secrets :: [SecretName] + , _TerraformExtendedConfig_dockers :: M.Map Name ([SecretName],[FolderDir]) + , _TerraformExtendedConfig_terraformConfig :: TerraformConfig + , _TerraformExtendedConfig_availableDisks :: [(DeviceName,VolumeName)] + } + +renderTerraform :: TerraformExtendedConfig -> Text +renderTerraform (TerraformExtendedConfig prepCmds cmds disks secrets _ tconf _) = + let prepProvisioner = renderProvider tconf $ nullRemoteProvsioner prepCmds + execProvisioner' = renderProvider tconf $ execProvisioner cmds + otherTemplates = renderProvider tconf $ defTemplates + (devNames, diskNames) = unzip disks + ebsVolumes = foldl (<>) T.empty $ zipWith3 awsEbsVolume (map (\i -> "atidot_ebs_vol_" ++ show i) ([1..] :: [Int])) devNames diskNames + secretsProvsioning = renderProvider tconf $ secretsProvisioner secrets + in foldl1 (<>) $ + [ otherTemplates + , ebsVolumes + , prepProvisioner + , secretsProvsioning + , execProvisioner' + ] + + +defTemplates :: String +defTemplates = foldl1 (<>) + [ provider + , awsVpc + , awsInternetGateway + , awsSubnet + , awsRouteTable + , awsRouteTableAssoc + , awsSecurityGroup + , awsEip + , awsKeyPair + , awsInstance + , envProvisioner + ] + + +awsInstance :: String +awsInstance = [r| +resource "aws_instance" "{{instanceName}}" { + ami = "ami-2757f631" + instance_type = "t2.micro" + subnet_id = aws_subnet.{{subnetName}}.id + key_name = "{{keyName}}" + vpc_security_group_ids = [ + aws_security_group.{{securityGroupName}}.id + ] +} + |] + + +envProvisioner :: String +envProvisioner = [r| +resource "null_resource" "env_setter" { + + triggers = { + public_ip = aws_eip.{{eipName}}.id + volume_id = aws_volume_attachment.{{ebsVolumeName}}.id + } + + connection { + user = "ubuntu" + host = aws_eip.{{eipName}}.public_ip + agent = false + private_key = file("~/.ssh/{{keyName}}") + } + + provisioner "remote-exec" { + inline = [ +"sudo apt update", +"sudo apt install docker.io -y", +"sudo usermod -aG docker $USER", +"sudo apt install jq -y", +"sudo apt install python3-pip -y", +"pip3 install awscli --upgrade --user", +"mkdir -p ~/.aws", +"printf \"[default]\naws_access_key_id = ${var.aws_access_key_id}\naws_secret_access_key = ${var.aws_secret_access_key}\n\" >> ~/.aws/credentials", +"printf \"[default]\nregion = ${var.aws_default_region}\noutput = ${var.aws_default_format}\n\" >> ~/.aws/config", +] + } + +} + |] + +secretsProvisioner :: [SecretName] -> String +secretsProvisioner [] = [] +secretsProvisioner secrets = [r| +resource "null_resource" "secrets_provisioner" { + + triggers = { + public_ip = aws_eip.{{eipName}}.id + volume_id = aws_volume_attachment.{{ebsVolumeName}}.id + env_setter = null_resource.env_setter.id + mount_and_pull = null_resource.mount_and_pull.id + + } + + connection { + user = "ubuntu" + host = aws_eip.{{eipName}}.public_ip + agent = false + private_key = file("~/.ssh/{{keyName}}") + } + + provisioner "remote-exec" { + inline = [|] <> + unlines (map commandifySecret secrets) + <> [r|] + } + +} + |] + where + commandifySecret :: String -> String + commandifySecret secret = [r| +"printf \"export |] <> secretifyName secret <> [r|=\" >> ~/.bashrc", +"printf \"\\$(/home/ubuntu/.local/bin/aws secretsmanager get-secret-value --secret-id |] <> secret <> [r| | jq '.SecretString')\" >> ~/.bashrc", +"printf \"\n\">> ~/.bashrc", +|] + + +nullRemoteProvsioner :: [Cmd] -> String +nullRemoteProvsioner cmds = [r| +resource "null_resource" "mount_and_pull" { + + triggers = { + public_ip = aws_eip.{{eipName}}.id + volume_id = aws_volume_attachment.{{ebsVolumeName}}.id + env_setter = null_resource.env_setter.id + + } + + connection { + user = "ubuntu" + host = aws_eip.{{eipName}}.public_ip + agent = false + private_key = file("~/.ssh/{{keyName}}") + } + |] <> + genExec + <> [r| +} + |] + where + + genExec :: String + genExec = [r| + provisioner "remote-exec" { + inline = [ +|] <> unlines ( map ((\l -> l <> ","). show) cmds) <> + [r| ] + } + |] + +execProvisioner :: [Cmd] -> String +execProvisioner cmds = [r| +resource "null_resource" "executor" { + + triggers = { + public_ip = aws_eip.{{eipName}}.id + volume_id = aws_volume_attachment.{{ebsVolumeName}}.id + env_setter = null_resource.env_setter.id + secrets_provisioner = null_resource.secrets_provisioner.id + + + } + + connection { + user = "ubuntu" + host = aws_eip.{{eipName}}.public_ip + agent = false + private_key = file("~/.ssh/{{keyName}}") + } + |] <> + genExec + <> [r| +} + |] + where + + genExec :: String + genExec = [r| + provisioner "remote-exec" { + inline = [ +|] <> unlines ( map ((\l -> l <> ","). show) cmds) <> + [r| ] + } + |] + + + +awsEbsVolume :: Name + -> DeviceName + -> VolumeName + -> Text +awsEbsVolume name deviceName volumeName = do + let ctx :: GVal (Run SourcePos (Writer Text) Text) + ctx = dict $ map (\(a,b) -> a ~> b) + [ ("name" , name ) + , ("deviceName" , deviceName ) + , ("volumeName" , volumeName ) + ] + easyRender ctx $ toTemplate template + where + template :: String + template = [r| +resource "aws_volume_attachment" "{{name}}" { + device_name = "/dev/{{deviceName}}" + volume_id = "{{volumeName}}" + instance_id = aws_instance.atidot-micro-instance.id + skip_destroy = true +} + |] + + +-- later, add the values from files +-- this part is used to config the aws cli to read from secrets + +awsConfigVars :: Text +awsConfigVars = [r| +variable "aws_access_key_id" { + type = string +} + +variable "aws_secret_access_key" { + type = string +} + +variable "aws_default_region" { + type = string + default = "us-east-1" +} + +variable "aws_default_format" { + type = string + default = "json" +} + |] + +replaceInvalidChars :: [Char] -> [Char] +replaceInvalidChars = + let repl '/' = '_' + repl c = c + in map repl + +secretifyName :: [Char] -> [Char] +secretifyName = map toUpper . replaceInvalidChars \ No newline at end of file diff --git a/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Test.hs b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Test.hs new file mode 100644 index 0000000..de680d2 --- /dev/null +++ b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Test.hs @@ -0,0 +1,34 @@ +module Atidot.Platform.Deployment.Interpreter.Test where + +-- import Control.Monad.State +import "free" Control.Monad.Free +-- import "free" Control.Monad.Free.TH +-- import Data.Map (Map) +-- import qualified Data.Map as M +import Atidot.Platform.Deployment +import "mtl" Control.Monad.State + +data TestConfig = + TestConfig + + +runTest :: TestConfig + -> DeploymentM a + -> IO () +runTest config dep = do + _ <- (runStateT (iterM run dep) config) + return () + where + run :: Deployment (StateT TestConfig IO a) -> StateT TestConfig IO a + run (Container _containerName next) = do + liftIO $ putStrLn "some container cmd" + next _containerName + run (Secret _secretData next) = do + liftIO $ putStrLn "some secret thingy" + next "" + run (Mount _disk next) = do + liftIO $ putStrLn "some storage mount" + next "some volumn mapping" + run (AttachSecret _ _ next) = next + run (AttachVolume _ _ next) = next + run (Execute _ _ _ next) = next \ No newline at end of file diff --git a/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Utils.hs b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Utils.hs new file mode 100644 index 0000000..56ca414 --- /dev/null +++ b/platform-deployment/src/Atidot/Platform/Deployment/Interpreter/Utils.hs @@ -0,0 +1,32 @@ +module Atidot.Platform.Deployment.Interpreter.Utils where + +import Prelude hiding (FilePath) +import "turtle" Turtle +import qualified "text" Data.Text as T + + +terraformDepDir :: FilePath +terraformDepDir = "terraform_dep" + +rSecretsDir :: Text +rSecretsDir = "/home/ubuntu/.secrets" + +reduceShell :: Shell Line -> IO Text +reduceShell = reduce $ Fold (<>) "" lineToText + +getPublicDns :: Text -> Text +getPublicDns = T.takeWhile (/= '"') + . T.tail . T.dropWhile (/= '"') + . snd + . T.breakOn "public_dns" + +getInstanceId :: Text -> Text +getInstanceId = T.takeWhile (/= '"') + . T.tail + . T.dropWhile (/= '"') + . snd + . T.breakOn "id" + . T.takeWhile (/= '}') + . T.dropWhile (/= '{') + . snd + . T.breakOn "aws_instance" \ No newline at end of file diff --git a/platform-terraform/.gitignore b/platform-terraform/.gitignore new file mode 100644 index 0000000..c368d45 --- /dev/null +++ b/platform-terraform/.gitignore @@ -0,0 +1,2 @@ +.stack-work/ +*~ \ No newline at end of file diff --git a/platform-terraform/LICENSE b/platform-terraform/LICENSE new file mode 100644 index 0000000..102126f --- /dev/null +++ b/platform-terraform/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2019 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/platform-terraform/app/Main.hs b/platform-terraform/app/Main.hs new file mode 100644 index 0000000..6750132 --- /dev/null +++ b/platform-terraform/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Atidot.Platform.Terraform + +main :: IO () +main = mkDep \ No newline at end of file diff --git a/platform-terraform/platform-terraform.cabal b/platform-terraform/platform-terraform.cabal new file mode 100644 index 0000000..7ac62b0 --- /dev/null +++ b/platform-terraform/platform-terraform.cabal @@ -0,0 +1,59 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.31.0. +-- +-- see: https://github.com/sol/hpack +-- +-- hash: dfaf9a2720a4a2aa73db935f57a73c6781e944f4bb8607566c551876db2254d4 + +name: platform-terraform +version: 0.1.0.0 +description: Please see the README on GitHub at +homepage: https://github.com/githubuser/platform-terraform#readme +bug-reports: https://github.com/githubuser/platform-terraform/issues +author: Author name here +maintainer: example@example.com +copyright: 2019 Author name here +license: BSD3 +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + ChangeLog.md + +source-repository head + type: git + location: https://github.com/githubuser/platform-terraform + +library + exposed-modules: + Atidot.Platform.Terraform + other-modules: + Paths_platform_terraform + hs-source-dirs: + src + default-extensions: PackageImports OverloadedStrings + build-depends: + base >=4.7 && <5 + , containers + , lens + , terraform-hs + , text + default-language: Haskell2010 + +executable platform-terraform-exe + main-is: Main.hs + other-modules: + Paths_platform_terraform + hs-source-dirs: + app + default-extensions: PackageImports OverloadedStrings + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: + base >=4.7 && <5 + , containers + , lens + , platform-terraform + , terraform-hs + , text + default-language: Haskell2010 diff --git a/platform-terraform/src/Atidot/Platform/Terraform.hs b/platform-terraform/src/Atidot/Platform/Terraform.hs new file mode 100644 index 0000000..1053663 --- /dev/null +++ b/platform-terraform/src/Atidot/Platform/Terraform.hs @@ -0,0 +1,65 @@ +module Atidot.Platform.Terraform where + +import "base" Control.Monad(void) +import "base" Data.Traversable(for) +import "base" Data.Monoid +import "lens" Control.Lens +import "terraform-hs" Language.Terraform.Core +import "terraform-hs" Language.Terraform.Aws + +import qualified "containers" Data.Map as M +import qualified "text" Data.Text as T +import qualified "terraform-hs" Language.Terraform.Util.Text as T + +simpleConfig :: T.Text -> TF () +simpleConfig zone = do + awsVpc <- awsVpc "atidot-vpc" "10.0.0.0/16" $ \vpcParams -> vpcParams{_vpc_enable_dns_support = True, _vpc_enable_dns_hostnames = True} + awsInternetGateway' "atidot-env-gw" (vpc_id awsVpc) + awsSn <- awsSubnet "atidot-subnet" (vpc_id awsVpc) "${cidrsubnet(aws_vpc.example_atidot-vpc.cidr_block, 3, 1)}" $ set sn_availability_zone (zone <> "a") -- <-- replace here after implementation + awsRt <- awsRouteTable "atidot-rt" (vpc_id awsVpc) $ \rtParams -> rtParams{_rt_tags = ("cidr_block" =: "0.0.0.0/0") <> ("gateway_id" =: "${aws_internet_gateway.example_atidot-env-gw.id}")} -- add route entry + awsRouteTableAssociation' "atidot-env-assoc" (sn_id awsSn) (rt_id awsRt) + awsSg <- awsSecurityGroup "atidot-sg" $ set sg_vpc_id (Just $ vpc_id awsVpc) + . set sg_ingress + [ ingressOnPort 22 + ] + . set sg_egress + [ egressAll + ] + + inst <- awsInstance "atidot-instance" "ami-2757f631" "t2.micro" $ set i_vpc_security_group_ids [sg_id awsSg] + . set i_subnet_id (Just $ sn_id awsSn) + awsEip "atidot-env" $ \eipParams -> eipParams{_eip_vpc = True, _eip_instance = Just $ (i_id inst)} + return () + + +ingressOnPort :: Int -> IngressRuleParams +ingressOnPort port = IngressRuleParams + { _ir_from_port = port + , _ir_to_port = port + , _ir_protocol = "tcp" + , _ir_cidr_blocks = ["0.0.0.0/0"] + } + +egressAll :: EgressRuleParams +egressAll = EgressRuleParams + { _er_from_port = 0 + , _er_to_port = 0 + , _er_protocol = "-1" + , _er_cidr_blocks = ["0.0.0.0/0"] + } + +(=:) = M.singleton + +-- to be added declaratively and replace the comment in line 20 +cidrSubnet :: AwsId AwsSubnet + -> Int + -> Int + -> String +cidrSubnet = undefined + +mkDep :: IO () +mkDep = generateFiles "" $ do + let zone = "us-east-1" + withNameScope "example" $ do + newAws (makeAwsParams zone){aws_profile="default"} + simpleConfig zone