packages feed

hercules-ci-cli-0.3.0: src/Hercules/CLI/Secret.hs

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE BlockArguments #-}

module Hercules.CLI.Secret where

import qualified Data.Aeson as A
import qualified Data.Aeson.Types as A
import qualified Data.Map as M
import qualified Data.Text as T
import Hercules.CLI.Common (exitMsg, runAuthenticated)
import Hercules.CLI.JSON as JSON
import Hercules.CLI.Options (mkCommand, subparser)
import Hercules.CLI.Project (ProjectPath (projectPathOwner, projectPathSite), getProjectPath, projectOption, projectPathProject)
import Hercules.Formats.Secret (Secret (Secret))
import qualified Hercules.Formats.Secret as Secret
import Hercules.UserException (UserException (UserException))
import qualified Options.Applicative as Optparse
import Protolude
import System.FilePath (takeDirectory, (</>))
import UnliftIO.Directory (XdgDirectory (XdgConfig), createDirectoryIfMissing, doesFileExist, getXdgDirectory)

commandParser, initLocal, add, echo :: Optparse.Parser (IO ())
commandParser =
  subparser
    ( mkCommand
        "init-local"
        (Optparse.progDesc "Create a local secrets file in ~/.config/hercules-ci/secrets/<site>/<owner>")
        initLocal
        <> mkCommand
          "add"
          (Optparse.progDesc "Insert a secret into the local secrets file")
          add
        <> mkCommand
          "echo"
          (Optparse.progDesc "Assemble a secret for stdout")
          echo
    )
initLocal = do
  projectOptionMaybe <- optional projectOption
  pure $ runAuthenticated do
    projectPath <- getProjectPath projectOptionMaybe
    secretsFilePath <- liftIO $ getSecretsFilePath projectPath
    doesFileExist secretsFilePath >>= \case
      True -> do
        putErrText $ "hci: Secrets file already existed. Path: " <> toS secretsFilePath
      False -> do
        liftIO $ createDirectoryIfMissing True (takeDirectory secretsFilePath)
        liftIO $ writeFile secretsFilePath "{}"
        putErrText $ "hci: Secrets file created. Path: " <> toS secretsFilePath
add = do
  secretName <- Optparse.strArgument (Optparse.metavar "SECRET_NAME" <> Optparse.help "Organization/account-wide name for the secret")
  mkJson <- JSON.options
  projectOptionMaybe <- optional projectOption
  pure $ runAuthenticated do
    secretDataValue <- liftIO (mkJson (Just secretName))
    secretData <- case A.parse A.parseJSON secretDataValue of
      A.Error e -> throwIO $ UserException $ "The secret data must be an object. " <> toS e
      A.Success a -> pure a
    projectPath <- getProjectPath projectOptionMaybe
    secretsFilePath <- liftIO $ getSecretsFilePath projectPath
    liftIO $
      doesFileExist secretsFilePath >>= \case
        False -> exitMsg $ "No secrets file found. If the account is correct, use `hci init-local`. (path: " <> toS secretsFilePath <> ")"
        True -> pass
    secrets <- liftIO $ readJsonFile secretsFilePath
    case M.lookup secretName secrets of
      Just _ -> do
        exitMsg $ "Secret " <> secretName <> " already exists in " <> toS secretsFilePath <> "."
      Nothing -> pass
    let secret =
          Secret
            { data_ = secretData,
              condition =
                Just $
                  Secret.And
                    [ Secret.IsOwner (projectPathOwner projectPath),
                      Secret.IsRepo (projectPathProject projectPath),
                      Secret.IsDefaultBranch
                    ]
            }

        secrets' = secrets & M.insert secretName (A.toJSON secret)
    liftIO $ writeJsonFile secretsFilePath secrets'
    putErrText $ "hci: Successfully wrote " <> secretName <> " to " <> toS secretsFilePath
    putErrText "     It is only available for the detected or passed project's default branch."
    putErrText "     You can edit the condition to suit your needs."
    putErrText "     NOTE: Remember to synchronize this file with your agents!"
echo = do
  mkJson <- JSON.options
  projectOptionMaybe <- optional projectOption
  pure do
    secretDataValue <- liftIO (mkJson Nothing)
    secretData <- case A.parse A.parseJSON secretDataValue of
      A.Error e -> throwIO $ UserException $ "The secret data must be an object. " <> toS e
      A.Success a -> pure a
    let secret =
          Secret
            { data_ = secretData,
              condition =
                projectOptionMaybe <&> \projectPath ->
                  Secret.And
                    [ Secret.IsOwner (projectPathOwner projectPath),
                      Secret.IsRepo (projectPathProject projectPath),
                      Secret.IsDefaultBranch
                    ]
            }
    liftIO $ JSON.printJson secret

getSecretsFilePath :: ProjectPath -> IO FilePath
getSecretsFilePath projectPath = do
  dir <- getXdgDirectory XdgConfig "hercules-ci"
  let toPathElement = toS . T.map (\case '/' -> '_'; x -> x)
  pure $ dir </> "secrets" </> toPathElement (projectPathSite projectPath) </> toPathElement (projectPathOwner projectPath) </> "secrets.json"