Add interactive init command (#224) (#227)

* Add interactive init command (#224)

* fixup! Add interactive init command (#224)

* fixup! Add interactive init command (#224)
This commit is contained in:
Gautier DI FOLCO 2023-10-25 04:03:44 +02:00 committed by GitHub
parent ccb8d7c3ba
commit a2aa3dba43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 145 additions and 17 deletions

View File

@ -52,7 +52,9 @@ optionParser = Opts
command "rollback" command "rollback"
(info rollbackParser (progDesc "Roll back to Nth previous release")) <> (info rollbackParser (progDesc "Roll back to Nth previous release")) <>
command "maintenance" command "maintenance"
(info maintenanceParser (progDesc "Enable/Disable maintenance mode")) (info maintenanceParser (progDesc "Enable/Disable maintenance mode")) <>
command "init"
(info initParser (progDesc "Initialize hapistrano file"))
) )
<*> strOption <*> strOption
( long "config" ( long "config"
@ -92,6 +94,9 @@ rollbackParser = Rollback
<> showDefault <> showDefault
<> help "How many deployments back to go?" ) <> help "How many deployments back to go?" )
initParser :: Parser Command
initParser = pure InitConfig
maintenanceParser :: Parser Command maintenanceParser :: Parser Command
maintenanceParser = maintenanceParser =
Maintenance Maintenance
@ -120,27 +125,35 @@ data Message
main :: IO () main :: IO ()
main = do main = do
Opts{..} <- execParser parserInfo opts@Opts{..} <- execParser parserInfo
case optsCommand of
Deploy cliReleaseFormat cliKeepReleases cliKeepOneFailed ->
runHapCmd opts $ \hapConfig@C.Config{..} executionMode ->
Hap.deploy
hapConfig
(fromMaybeReleaseFormat cliReleaseFormat configReleaseFormat)
(fromMaybeKeepReleases cliKeepReleases configKeepReleases)
(cliKeepOneFailed || configKeepOneFailed)
executionMode
Rollback n ->
runHapCmd opts $ \C.Config{..} _ ->
Hap.rollback configTargetSystem configDeployPath n configRestartCommand
Maintenance Enable ->
runHapCmd opts $ \C.Config{..} _ ->
Hap.writeMaintenanceFile configDeployPath configMaintenanceDirectory configMaintenanceFileName
Maintenance _ ->
runHapCmd opts $ \C.Config{..} _ ->
Hap.deleteMaintenanceFile configDeployPath configMaintenanceDirectory configMaintenanceFileName
InitConfig -> Hap.initConfig getLine
runHapCmd :: Opts -> (C.Config -> C.ExecutionMode -> Hapistrano ()) -> IO ()
runHapCmd Opts{..} hapCmd = do
hapConfig@C.Config{..} <- Yaml.loadYamlSettings [optsConfigFile] [] Yaml.useEnv hapConfig@C.Config{..} <- Yaml.loadYamlSettings [optsConfigFile] [] Yaml.useEnv
chan <- newTChanIO chan <- newTChanIO
let printFnc dest str = atomically $ let printFnc dest str = atomically $
writeTChan chan (PrintMsg dest str) writeTChan chan (PrintMsg dest str)
hap shell sshOpts executionMode = do hap shell sshOpts executionMode = do
r <- Hap.runHapistrano sshOpts shell printFnc $ r <- Hap.runHapistrano sshOpts shell printFnc $ hapCmd hapConfig executionMode
case optsCommand of
Deploy cliReleaseFormat cliKeepReleases cliKeepOneFailed ->
Hap.deploy
hapConfig
(fromMaybeReleaseFormat cliReleaseFormat configReleaseFormat)
(fromMaybeKeepReleases cliKeepReleases configKeepReleases)
(cliKeepOneFailed || configKeepOneFailed)
executionMode
Rollback n ->
Hap.rollback configTargetSystem configDeployPath n configRestartCommand
Maintenance Enable-> do
Hap.writeMaintenanceFile configDeployPath configMaintenanceDirectory configMaintenanceFileName
Maintenance _ -> do
Hap.deleteMaintenanceFile configDeployPath configMaintenanceDirectory configMaintenanceFileName
atomically (writeTChan chan FinishMsg) atomically (writeTChan chan FinishMsg)
return r return r
printer :: Int -> IO () printer :: Int -> IO ()
@ -162,6 +175,7 @@ main = do
hap targetShell (Just $ SshOptions targetHost targetPort targetSshArgs) hap targetShell (Just $ SshOptions targetHost targetPort targetSshArgs)
(if leadTarget == currentTarget then C.LeadTarget else C.AllTargets) (if leadTarget == currentTarget then C.LeadTarget else C.AllTargets)
in runHap <$> targets in runHap <$> targets
results <- (runConcurrently . traverse Concurrently) results <- (runConcurrently . traverse Concurrently)
((Right () <$ printer (length haps)) : haps) ((Right () <$ printer (length haps)) : haps)
case sequence_ results of case sequence_ results of

View File

@ -54,6 +54,7 @@ library
build-depends: aeson >= 2.0 && < 3.0 build-depends: aeson >= 2.0 && < 3.0
, ansi-terminal >= 0.9 && < 0.12 , ansi-terminal >= 0.9 && < 0.12
, base >= 4.9 && < 5.0 , base >= 4.9 && < 5.0
, directory >= 1.2.5 && < 1.4
, filepath >= 1.2 && < 1.5 , filepath >= 1.2 && < 1.5
, gitrev >= 1.2 && < 1.4 , gitrev >= 1.2 && < 1.4
, mtl >= 2.0 && < 3.0 , mtl >= 2.0 && < 3.0
@ -61,6 +62,7 @@ library
, path >= 0.5 && < 0.9 , path >= 0.5 && < 0.9
, path-io >= 1.2 && < 1.7 , path-io >= 1.2 && < 1.7
, process >= 1.4 && < 1.7 , process >= 1.4 && < 1.7
, text >= 1.2 && < 3
, typed-process >= 0.2 && < 0.3 , typed-process >= 0.2 && < 0.3
, time >= 1.5 && < 1.11 , time >= 1.5 && < 1.11
, transformers >= 0.4 && < 0.6 , transformers >= 0.4 && < 0.6
@ -99,6 +101,7 @@ test-suite test
main-is: Spec.hs main-is: Spec.hs
other-modules: System.HapistranoSpec other-modules: System.HapistranoSpec
, System.Hapistrano.ConfigSpec , System.Hapistrano.ConfigSpec
, System.Hapistrano.InitSpec
, System.HapistranoPropsSpec , System.HapistranoPropsSpec
build-depends: base >= 4.9 && < 5.0 build-depends: base >= 4.9 && < 5.0
, aeson , aeson

View File

@ -0,0 +1,17 @@
module System.Hapistrano.InitSpec (spec) where
import Test.Hspec
import System.Directory (doesFileExist, getCurrentDirectory, withCurrentDirectory)
import System.FilePath ((</>))
import System.Hapistrano (initConfig)
import System.IO.Temp (withSystemTempDirectory)
spec :: Spec
spec = do
describe "initConfig" $ do
it "should create a file when missing" $
withSystemTempDirectory "hapistrano-spec-initConfig-missing" $ \tempDir ->
withCurrentDirectory tempDir $ do
configFilePath <- (</> "hap.yml") <$> getCurrentDirectory
initConfig $ return ""
doesFileExist configFilePath `shouldReturn` True

View File

@ -9,6 +9,7 @@
-- A module for creating reliable deploy processes for Haskell applications. -- A module for creating reliable deploy processes for Haskell applications.
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
@ -26,6 +27,7 @@ module System.Hapistrano
, dropOldReleases , dropOldReleases
, playScript , playScript
, playScriptLocally , playScriptLocally
, initConfig
-- * Path helpers -- * Path helpers
, releasePath , releasePath
, sharedPath , sharedPath
@ -42,7 +44,10 @@ import Control.Monad.Reader (local)
import Data.List (dropWhileEnd, genericDrop, sortOn) import Data.List (dropWhileEnd, genericDrop, sortOn)
import Data.Maybe (fromMaybe, mapMaybe) import Data.Maybe (fromMaybe, mapMaybe)
import Data.Ord (Down (..)) import Data.Ord (Down (..))
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Time import Data.Time
import qualified Data.Yaml as Yaml
import Numeric.Natural import Numeric.Natural
import Path import Path
import Path.IO import Path.IO
@ -55,6 +60,10 @@ import System.Hapistrano.Config ( BuildCommand (..)
import qualified System.Hapistrano.Config as HC import qualified System.Hapistrano.Config as HC
import System.Hapistrano.Core import System.Hapistrano.Core
import System.Hapistrano.Types import System.Hapistrano.Types
import qualified System.Directory as Directory
import System.Exit (exitFailure)
import qualified System.FilePath as FilePath
import System.IO (stderr)
import Text.Read (readMaybe) import Text.Read (readMaybe)
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
@ -262,6 +271,41 @@ playScriptLocally cmds =
}) $ }) $
forM_ cmds $ flip execWithInheritStdout Nothing forM_ cmds $ flip execWithInheritStdout Nothing
initConfig :: IO String -> IO ()
initConfig getLine' = do
configFilePath <- (FilePath.</> "hap.yml") <$> Directory.getCurrentDirectory
alreadyExisting <- Directory.doesFileExist configFilePath
when alreadyExisting $ do
T.hPutStrLn stderr "'hap.yml' already exists"
exitFailure
putStrLn "Creating 'hap.yml'"
defaults <- defaultInitTemplateConfig
let prompt :: Read a => T.Text -> a -> IO a
prompt title d = do
T.putStrLn $ title <> "?: "
x <- getLine'
return $
if null x
then d
else read x
prompt' :: Read a => T.Text -> (InitTemplateConfig -> T.Text) -> (InitTemplateConfig -> a) -> IO a
prompt' title f fd = prompt (title <> " (default: " <> f defaults <> ")") (fd defaults)
let yesNo :: a -> a -> T.Text -> a
yesNo t f x = if x == "y" then t else f
config <-
InitTemplateConfig
<$> prompt' "repo" repo repo
<*> prompt' "revision" revision revision
<*> prompt' "host" host host
<*> prompt' "port" (T.pack . show . port) port
<*> return (buildScript defaults)
<*> fmap (yesNo (restartCommand defaults) Nothing) (prompt' "Include restart command" (const "Y/n") (const "y"))
Yaml.encodeFile configFilePath config
putStrLn $ "Configuration written at " <> configFilePath
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
-- Helpers -- Helpers

View File

@ -28,6 +28,7 @@ module System.Hapistrano.Types
, Opts(..) , Opts(..)
, Command(..) , Command(..)
, MaintenanceOptions(..) , MaintenanceOptions(..)
, InitTemplateConfig(..)
-- * Types helpers -- * Types helpers
, mkRelease , mkRelease
, releaseTime , releaseTime
@ -36,6 +37,7 @@ module System.Hapistrano.Types
, fromMaybeReleaseFormat , fromMaybeReleaseFormat
, fromMaybeKeepReleases , fromMaybeKeepReleases
, toMaybePath , toMaybePath
, defaultInitTemplateConfig
) where ) where
import Control.Applicative import Control.Applicative
@ -44,9 +46,14 @@ import Control.Monad.Except
import Control.Monad.Reader import Control.Monad.Reader
import Data.Aeson import Data.Aeson
import Data.Maybe import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
import Data.Time import Data.Time
import Numeric.Natural import Numeric.Natural
import Path import Path
import System.Exit (ExitCode(ExitSuccess))
import System.Process.Typed (nullStream, readProcessStdout, setStderr, shell)
-- | Hapistrano monad. -- | Hapistrano monad.
newtype Hapistrano a = newtype Hapistrano a =
@ -192,6 +199,7 @@ data Command
-- get deleted or not) -- get deleted or not)
| Rollback Natural -- ^ Rollback to Nth previous release | Rollback Natural -- ^ Rollback to Nth previous release
| Maintenance MaintenanceOptions | Maintenance MaintenanceOptions
| InitConfig -- ^ initialize configuration file
-- | Create a 'Release' indentifier. -- | Create a 'Release' indentifier.
mkRelease :: ReleaseFormat -> UTCTime -> Release mkRelease :: ReleaseFormat -> UTCTime -> Release
@ -210,6 +218,48 @@ renderRelease (Release rfmt time) = formatTime defaultTimeLocale fmt time
ReleaseShort -> releaseFormatShort ReleaseShort -> releaseFormatShort
ReleaseLong -> releaseFormatLong ReleaseLong -> releaseFormatLong
-- | Initial configurable fields
data InitTemplateConfig = InitTemplateConfig
{ repo :: T.Text
, revision :: T.Text
, host :: T.Text
, port :: Word
, buildScript :: [T.Text]
, restartCommand :: Maybe T.Text
}
defaultInitTemplateConfig :: IO InitTemplateConfig
defaultInitTemplateConfig = do
let shellWithDefault d cmd = do
(exitCode, stdout) <- readProcessStdout $ setStderr nullStream $ shell cmd
return $
if exitCode == ExitSuccess
then maybe d (T.strip . TL.toStrict) $ listToMaybe $ TL.lines $ TL.decodeUtf8 stdout
else d
remoteBranch <- shellWithDefault "origin/main" "git rev-parse --abbrev-ref --symbolic-full-name @{u}"
let remote = T.takeWhile (/='/') remoteBranch
repository <- shellWithDefault "https://github.com/user/repo.git" ("git ls-remote --get-url " <> T.unpack remote)
return $
InitTemplateConfig
{ repo = repository
, revision = remoteBranch
, host = "root@localhost"
, port = 22
, buildScript = ["echo 'Build steps'"]
, restartCommand = Just "echo 'Restart command'"
}
instance ToJSON InitTemplateConfig where
toJSON x =
object
[ "repo" .= repo x
, "revision" .= revision x
, "host" .= host x
, "port" .= port x
, "buildScript" .= buildScript x
, "restartCommand" .= restartCommand x
]
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
-- Types helpers -- Types helpers