Skip to content
Snippets Groups Projects
Commit 7fbe74d4 authored by Akshay Mankar's avatar Akshay Mankar
Browse files

Basic implementation using Servant and Shelly

parents
No related branches found
No related tags found
No related merge requests found
.envrc 0 → 100644
env="$(nix-build $PWD/direnv.nix -A env)"
# PATH_add "${env}/bin"
load_prefix "${env}"
result
dist-newstyle
.dir-locals.el
\ No newline at end of file
# Revision history for terraform-http-pass-backend
## 0.1.0.0 -- YYYY-mm-dd
* First version. Released on an unsuspecting world.
import Distribution.Simple
main = defaultMain
module Main where
import Terraform.HttpBackend.Pass.Run (run)
main :: IO ()
main = run
let
sources = import ./nix/sources.nix;
pkgs = import sources.nixpkgs {};
in {
env = pkgs.buildEnv {
name = "terraform-http-pass-backend";
paths = with pkgs; [
pass
niv
gnumake
haskell-language-server
cabal-install
haskell.compiler.ghc8103 # HLS doesn't support GHC 9 yet.
zlib.dev
zlib
];
};
}
{
"niv": {
"branch": "master",
"description": "Easy dependency management for Nix projects",
"homepage": "https://github.com/nmattia/niv",
"owner": "nmattia",
"repo": "niv",
"rev": "af958e8057f345ee1aca714c1247ef3ba1c15f5e",
"sha256": "1qjavxabbrsh73yck5dcq8jggvh3r2jkbr6b5nlz5d9yrqm9255n",
"type": "tarball",
"url": "https://github.com/nmattia/niv/archive/af958e8057f345ee1aca714c1247ef3ba1c15f5e.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"nixpkgs": {
"branch": "nixpkgs-unstable",
"description": "Nix Packages collection",
"homepage": "",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8542021fe7f9f777294649b1cdc853240806ff21",
"sha256": "1madhksbsdiyszpnncrhh1i32z1w7jrfcblmpi3llq90j7yjg6a1",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/8542021fe7f9f777294649b1cdc853240806ff21.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}
# This file has been generated by Niv.
let
#
# The fetchers. fetch_<type> fetches specs of type <type>.
#
fetch_file = pkgs: spec:
if spec.builtin or true then
builtins_fetchurl { inherit (spec) url sha256; }
else
pkgs.fetchurl { inherit (spec) url sha256; };
fetch_tarball = pkgs: name: spec:
let
ok = str: ! builtins.isNull (builtins.match "[a-zA-Z0-9+-._?=]" str);
# sanitize the name, though nix will still fail if name starts with period
name' = stringAsChars (x: if ! ok x then "-" else x) "${name}-src";
in
if spec.builtin or true then
builtins_fetchTarball { name = name'; inherit (spec) url sha256; }
else
pkgs.fetchzip { name = name'; inherit (spec) url sha256; };
fetch_git = spec:
builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; };
fetch_local = spec: spec.path;
fetch_builtin-tarball = name: throw
''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=tarball -a builtin=true'';
fetch_builtin-url = name: throw
''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=file -a builtin=true'';
#
# Various helpers
#
# The set of packages used when specs are fetched using non-builtins.
mkPkgs = sources:
let
sourcesNixpkgs =
import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) {};
hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
hasThisAsNixpkgsPath = <nixpkgs> == ./.;
in
if builtins.hasAttr "nixpkgs" sources
then sourcesNixpkgs
else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
import <nixpkgs> {}
else
abort
''
Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
add a package called "nixpkgs" to your sources.json.
'';
# The actual fetching function.
fetch = pkgs: name: spec:
if ! builtins.hasAttr "type" spec then
abort "ERROR: niv spec ${name} does not have a 'type' attribute"
else if spec.type == "file" then fetch_file pkgs spec
else if spec.type == "tarball" then fetch_tarball pkgs name spec
else if spec.type == "git" then fetch_git spec
else if spec.type == "local" then fetch_local spec
else if spec.type == "builtin-tarball" then fetch_builtin-tarball name
else if spec.type == "builtin-url" then fetch_builtin-url name
else
abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
# If the environment variable NIV_OVERRIDE_${name} is set, then use
# the path directly as opposed to the fetched source.
replace = name: drv:
let
saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name;
ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}";
in
if ersatz == "" then drv else ersatz;
# Ports of functions for older nix versions
# a Nix version of mapAttrs if the built-in doesn't exist
mapAttrs = builtins.mapAttrs or (
f: set: with builtins;
listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
);
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1);
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257
stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
concatStrings = builtins.concatStringsSep "";
# fetchTarball version that is compatible between all the versions of Nix
builtins_fetchTarball = { url, name, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchTarball;
in
if lessThan nixVersion "1.12" then
fetchTarball { inherit name url; }
else
fetchTarball attrs;
# fetchurl version that is compatible between all the versions of Nix
builtins_fetchurl = { url, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchurl;
in
if lessThan nixVersion "1.12" then
fetchurl { inherit url; }
else
fetchurl attrs;
# Create the final "sources" from the config
mkSources = config:
mapAttrs (
name: spec:
if builtins.hasAttr "outPath" spec
then abort
"The values in sources.json should not have an 'outPath' attribute"
else
spec // { outPath = replace name (fetch config.pkgs name spec); }
) config.sources;
# The "config" used by the fetchers
mkConfig =
{ sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null
, sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile)
, pkgs ? mkPkgs sources
}: rec {
# The sources, i.e. the attribute set of spec name to spec
inherit sources;
# The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
inherit pkgs;
};
in
mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); }
{ nixpkgs ? import <nixpkgs> {}, compiler ? "default", doBenchmark ? false }:
let
inherit (nixpkgs) pkgs;
f = { mkDerivation, base, servant, servant-server, shelly, stdenv
, text, zlib
}:
mkDerivation {
pname = "terraform-http-pass-backend";
version = "0.1.0.0";
src = ./.;
isLibrary = true;
isExecutable = true;
libraryHaskellDepends = [
base servant servant-server shelly text
];
librarySystemDepends = [ zlib ];
executableHaskellDepends = [ base ];
license = "unknown";
hydraPlatforms = stdenv.lib.platforms.none;
};
haskellPackages = if compiler == "default"
then pkgs.haskellPackages
else pkgs.haskell.packages.${compiler};
variant = if doBenchmark then pkgs.haskell.lib.doBenchmark else pkgs.lib.id;
drv = variant (haskellPackages.callPackage f {});
in
if pkgs.lib.inNixShell then drv.env else drv
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
module Terraform.HttpBackend.Pass.Api where
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import GHC.Generics (Generic)
import Servant
import Servant.API.Generic (ToServant, ToServantApi)
import qualified Servant.Server.Generic as Servant
import Terraform.HttpBackend.Pass.App (AppT)
import Terraform.HttpBackend.Pass.Crypt (MonadPass (..))
import Terraform.HttpBackend.Pass.Git (MonadGit (..))
type GetState = "state" :> Capture "name" Text :> Get '[PlainText] Text
type UpdateState = "state" :> Capture "name" Text :> ReqBody '[PlainText] Text :> PostNoContent
type DeleteState = "state" :> Capture "name" Text :> Delete '[PlainText] Text
type Api = GetState :<|> UpdateState :<|> DeleteState
api :: Proxy Api
api = Proxy
server :: (Monad m, MonadPass m, MonadGit m) => ServerT Api m
server =
getStateImpl
:<|> updateStateImpl
:<|> purgeStateImpl
-- TODO: Gracefully return 404 when the file doesn't exist
getStateImpl :: (Monad m, MonadGit m, MonadPass m) => Text -> m Text
getStateImpl name = do
gitPull
decrypt (name <> "/terraform.tfstate")
updateStateImpl :: (Monad m, MonadPass m, MonadGit m) => Text -> Text -> m NoContent
updateStateImpl name tfstate = do
gitPull
let path = stateFilePath name
-- Also commits
encrypt path tfstate
gitPush
pure NoContent
purgeStateImpl :: (MonadGit m, MonadPass m, Monad m) => Text -> m Text
purgeStateImpl name = do
tfstate <- getStateImpl name
gitPush
pure tfstate
stateFilePath :: Text -> Text
stateFilePath name = name <> "/terraform.tfstate"
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Terraform.HttpBackend.Pass.App where
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Reader (MonadReader (ask), ReaderT (runReaderT))
import Data.Text (Text)
import qualified Data.Text as Text
import Shelly (shelly)
import qualified Shelly
import Terraform.HttpBackend.Pass.Crypt (MonadPass (..))
import Terraform.HttpBackend.Pass.Env (Env (..))
import Terraform.HttpBackend.Pass.Git (MonadGit (..))
newtype AppT m a = AppT {unAppT :: ReaderT Env m a}
deriving newtype (Functor, Applicative, Monad, MonadReader Env, MonadIO)
instance MonadIO m => MonadGit (AppT m) where
gitAdd path = runGit_ ["add", path]
gitCommit message = runGit_ ["commit", "-m", message]
gitPush = runGit_ ["push"]
gitPull = runGit_ ["pull", "--rebase"]
gitRm path = runGit_ ["rm", path]
runGit_ :: (MonadIO m, MonadReader Env m) => [Text] -> m ()
runGit_ args = do
Env {..} <- ask
shelly $ Shelly.run_ "git" (["-C", Text.pack directory] ++ args)
instance (Monad m, MonadIO m) => MonadPass (AppT m) where
encrypt name secret = do
Env {..} <- ask
shelly $ do
Shelly.setenv "PASSWORD_STORE_DIR" (Text.pack directory)
Shelly.setStdin secret
Shelly.run_ "pass" ["insert", "-m", name]
decrypt name = do
Env {..} <- ask
shelly $ do
Shelly.setenv "PASSWORD_STORE_DIR" (Text.pack directory)
Shelly.run "pass" [name]
purge name = do
Env {..} <- ask
shelly $ do
Shelly.setenv "PASSWORD_STORE_DIR" (Text.pack directory)
Shelly.run_ "pass" ["rm", name]
runAppT :: Env -> AppT m a -> m a
runAppT env (AppT r) = runReaderT r env
module Terraform.HttpBackend.Pass.Crypt where
import Data.Text (Text)
class MonadPass m where
encrypt :: Text -> Text -> m ()
decrypt :: Text -> m Text
purge :: Text -> m ()
{-# LANGUAGE RecordWildCards #-}
module Terraform.HttpBackend.Pass.Env where
import Terraform.HttpBackend.Pass.Options (Options(..))
newtype Env = Env { directory :: FilePath}
mkEnv :: Options -> Env
mkEnv Options {..}= Env { directory = repositoryPath }
module Terraform.HttpBackend.Pass.Git where
import Data.Text (Text)
class MonadGit m where
gitAdd :: Text -> m ()
gitCommit :: Text -> m ()
gitPush :: m ()
gitPull :: m ()
gitRm :: Text -> m ()
{-# LANGUAGE DeriveGeneric #-}
module Terraform.HttpBackend.Pass.Options where
import GHC.Generics (Generic)
import Options.Applicative
import Options.Generic (ParseRecord)
data Options = Options {repositoryPath :: FilePath, port :: Int}
deriving (Generic)
instance ParseRecord Options
{-# LANGUAGE OverloadedStrings #-}
module Terraform.HttpBackend.Pass.Run where
import qualified Network.Wai.Handler.Warp as Warp
import Options.Generic
import qualified Servant.Server as Servant
import qualified Terraform.HttpBackend.Pass.Api as Api
import Terraform.HttpBackend.Pass.App (runAppT)
import Terraform.HttpBackend.Pass.Env (Env, mkEnv)
import qualified Terraform.HttpBackend.Pass.Options as Options
run :: IO ()
run = do
opts <- getRecord "Terraform HTTP Backend using Pass and Git"
let env = mkEnv opts
Warp.run (Options.port opts) (Servant.serve Api.api (hoistServer env))
hoistServer :: Env -> Servant.ServerT Api.Api Servant.Handler
hoistServer env = Servant.hoistServer Api.api (runAppT env) Api.server
cabal-version: >=1.10
name: terraform-http-backend-pass
version: 0.1.0.0
license-file: LICENSE
author: Akshay Mankar
maintainer: itsakshaymankar@gmail.com
build-type: Simple
extra-source-files: CHANGELOG.md
executable terraform-http-backend-pass
main-is: Main.hs
hs-source-dirs: app
build-depends: base >=4.14 && <5
, terraform-http-backend-pass
default-language: Haskell2010
library
hs-source-dirs: src
default-language: Haskell2010
extra-libraries: z
build-depends: base >= 4.14 && <5
, bytestring
, mtl
, optparse-applicative
, optparse-generic
, servant
, servant-server
, shelly
, text
, warp
exposed-modules: Terraform.HttpBackend.Pass.Api
, Terraform.HttpBackend.Pass.App
, Terraform.HttpBackend.Pass.Crypt
, Terraform.HttpBackend.Pass.Env
, Terraform.HttpBackend.Pass.Git
, Terraform.HttpBackend.Pass.Options
, Terraform.HttpBackend.Pass.Run
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment