packages feed

hercules-ci-agent-0.10.2: test/Hercules/Agent/ConfigSpec.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module Hercules.Agent.ConfigSpec (spec) where

import Data.Aeson qualified as A
import Data.Map qualified as M
import Data.Text qualified as T
import Hercules.Agent.Config (Config (..), Purpose (Input), combiCodec)
import Hercules.Agent.Config.Combined (forJson, forToml)
import Hercules.Agent.Config.Json qualified as Json
import Hercules.CNix.Verbosity (Verbosity (Vomit))
import Hercules.Formats.Mountable (Mountable (Mountable))
import Hercules.Formats.Mountable qualified
import Hercules.Formats.Secret qualified
import Katip (Severity (DebugS))
import Protolude
import Test.Hspec
import Toml.Codec.Code

deriving instance Eq (Config 'Input)

deriving instance Show (Config 'Input)

spec :: Spec
spec =
  describe "Config" $ do
    describe "toml codec" $ do
      -- note that this hooks into a more strict version of the parser
      it "parsers example 1 strictly" $ do
        let input =
              T.unlines
                [ "allowInsecureBuiltinFetchers = true",
                  "apiBaseUrl = \"http://api\"",
                  "baseDirectory = \"/var/lib/hercules-ci-agent\"",
                  "binaryCachesPath = \"/nix/store/9cbyq6f2ajxm64rvffhy8ndi73mbg5zd-binary-caches.json\"",
                  "clusterJoinTokenPath = \"/nix/store/y0200pr82sczzla5jd4pzkb8idnqhxkj-pretend-agent-token\"",
                  "concurrentTasks = 4",
                  "logLevel = \"DebugS\"",
                  "nixUserIsTrusted = true",
                  "secretsJsonPath = \"/var/lib/hercules-ci-agent/secrets/secrets.json\"",
                  "staticSecretsDirectory = \"/var/lib/hercules-ci-agent/secrets\"",
                  "workDirectory = \"/var/lib/hercules-ci-agent/work\"",
                  "remotePlatformsWithSameFeatures = [\"aarch64-darwin\"]",
                  "nixVerbosity = \"vomit\"",
                  "[nixSettings]",
                  "  substituters = \"https://example.com\"",
                  -- This line should not be needed!
                  "[effectMountables]",
                  "[effectMountables.hosts]",
                  "  condition = true",
                  "  readOnly = true",
                  "  source = \"/etc/hosts\"",
                  "[labels]",
                  "module = \"nixos-profile\"",
                  "[labels.agent]",
                  "source = \"flake\"",
                  "[labels.lib]",
                  "version = \"24.05pre-git\""
                ]
        decode (forToml combiCodec) input
          `shouldBe` Right
            ( Config
                { herculesApiBaseURL = Just "http://api",
                  nixUserIsTrusted = Just True,
                  concurrentTasks = Just (Right 4),
                  baseDirectory = Just "/var/lib/hercules-ci-agent",
                  staticSecretsDirectory = Just "/var/lib/hercules-ci-agent/secrets",
                  workDirectory = Just "/var/lib/hercules-ci-agent/work",
                  clusterJoinTokenPath = Just "/nix/store/y0200pr82sczzla5jd4pzkb8idnqhxkj-pretend-agent-token",
                  binaryCachesPath = Just "/nix/store/9cbyq6f2ajxm64rvffhy8ndi73mbg5zd-binary-caches.json",
                  secretsJsonPath = Just "/var/lib/hercules-ci-agent/secrets/secrets.json",
                  logLevel = Just DebugS,
                  nixVerbosity = Nothing, -- FIXME: should be Just "vomit"
                  labels =
                    Just
                      ( M.fromList
                          [ ("agent", (A.object [("source", A.String "flake")])),
                            ("lib", A.object ([("version", A.String "24.05pre-git")])),
                            ("module", A.String "nixos-profile")
                          ]
                      ),
                  allowInsecureBuiltinFetchers = Just True,
                  remotePlatformsWithSameFeatures = Just ["aarch64-darwin"],
                  effectMountables =
                    M.fromList
                      [ ( "hosts",
                          Mountable
                            { source = "/etc/hosts",
                              readOnly = True,
                              condition = Hercules.Formats.Secret.Const True
                            }
                        )
                      ],
                  nixSettings = M.singleton "substituters" "https://example.com"
                }
            )
      it "parses empty config" $ do
        let input = T.unlines []
        decode (forToml combiCodec) input
          `shouldBe` Right
            emptyConfig
              { -- sure, why not
                labels = Just mempty
              }

    describe "json codec" $ do
      it "parses example 1" $ do
        let input =
              T.unlines
                [ "{",
                  "\"apiBaseUrl\": \"http://api\",",
                  "\"nixUserIsTrusted\": true,",
                  "\"concurrentTasks\": 4,",
                  "\"baseDirectory\": \"/var/lib/hercules-ci-agent\",",
                  "\"staticSecretsDirectory\": \"/var/lib/hercules-ci-agent/secrets\",",
                  "\"workDirectory\": \"/var/lib/hercules-ci-agent/work\",",
                  "\"clusterJoinTokenPath\": \"/nix/store/y0200pr82sczzla5jd4pzkb8idnqhxkj-pretend-agent-token\",",
                  "\"binaryCachesPath\": \"/nix/store/9cbyq6f2ajxm64rvffhy8ndi73mbg5zd-binary-caches.json\",",
                  "\"secretsJsonPath\": \"/var/lib/hercules-ci-agent/secrets/secrets.json\",",
                  "\"logLevel\": \"DebugS\",",
                  "\"nixVerbosity\": \"Vomit\",",
                  "\"nixSettings\": { \"substituters\": \"https://example.com\" },",
                  "\"remotePlatformsWithSameFeatures\": [\"aarch64-darwin\"],",
                  "\"labels\": {",
                  "  \"agent\": {\"source\": \"flake\"},",
                  "  \"lib\": {\"version\": \"24.05pre-git\"},",
                  "  \"module\": \"nixos-profile\"",
                  "},",
                  "\"effectMountables\": {",
                  "  \"hosts\": {",
                  "    \"source\": \"/etc/hosts\",",
                  "    \"readOnly\": true,",
                  "    \"condition\": true,",
                  "    \"and\":[]",
                  "  }",
                  "},",
                  "\"allowInsecureBuiltinFetchers\": true",
                  "}"
                ]
        Json.decode (forJson combiCodec) (toS $ encodeUtf8 input)
          `shouldBe` Right
            Config
              { herculesApiBaseURL = Just "http://api",
                nixUserIsTrusted = Just True,
                concurrentTasks = Just (Right 4),
                baseDirectory = Just "/var/lib/hercules-ci-agent",
                staticSecretsDirectory = Just "/var/lib/hercules-ci-agent/secrets",
                workDirectory = Just "/var/lib/hercules-ci-agent/work",
                clusterJoinTokenPath = Just "/nix/store/y0200pr82sczzla5jd4pzkb8idnqhxkj-pretend-agent-token",
                binaryCachesPath = Just "/nix/store/9cbyq6f2ajxm64rvffhy8ndi73mbg5zd-binary-caches.json",
                secretsJsonPath = Just "/var/lib/hercules-ci-agent/secrets/secrets.json",
                logLevel = Just DebugS,
                nixVerbosity = Just Vomit,
                labels =
                  Just
                    ( M.fromList
                        [ ("agent", (A.object [("source", A.String "flake")])),
                          ("lib", A.object ([("version", A.String "24.05pre-git")])),
                          ("module", A.String "nixos-profile")
                        ]
                    ),
                allowInsecureBuiltinFetchers = Just True,
                remotePlatformsWithSameFeatures = Just ["aarch64-darwin"],
                effectMountables =
                  M.fromList
                    [ ( "hosts",
                        Mountable
                          { source = "/etc/hosts",
                            readOnly = True,
                            condition = Hercules.Formats.Secret.Const True
                          }
                      )
                    ],
                nixSettings = M.singleton "substituters" "https://example.com"
              }

      it "handles empty config" $ do
        let inputs =
              T.unlines
                [ "{",
                  "}"
                ]
        Json.decode (forJson combiCodec) (toS $ encodeUtf8 inputs) `shouldBe` Right emptyConfig

      it "handles nulls" $ do
        let inputs =
              T.unlines
                [ "{",
                  "\"apiBaseUrl\": null,",
                  "\"nixUserIsTrusted\": null,",
                  "\"concurrentTasks\": null,",
                  "\"baseDirectory\": null,",
                  "\"staticSecretsDirectory\": null,",
                  "\"workDirectory\": null,",
                  "\"clusterJoinTokenPath\": null,",
                  "\"binaryCachesPath\": null,",
                  "\"secretsJsonPath\": null,",
                  "\"logLevel\": null,",
                  "\"labels\": null,",
                  "\"allowInsecureBuiltinFetchers\": null",
                  "}"
                ]
        Json.decode (forJson combiCodec) (toS $ encodeUtf8 inputs) `shouldBe` Right emptyConfig

      it "allows use of null in labels" $ do
        let inputs =
              T.unlines
                [ "{",
                  "\"labels\": {",
                  "  \"version\": null",
                  "}",
                  "}"
                ]
        Json.decode (forJson combiCodec) (toS $ encodeUtf8 inputs)
          `shouldBe` Right
            emptyConfig
              { labels = Just (M.singleton "version" A.Null)
              }

emptyConfig :: Config 'Input
emptyConfig =
  Config
    { herculesApiBaseURL = Nothing,
      nixUserIsTrusted = Nothing,
      concurrentTasks = Nothing,
      baseDirectory = Nothing,
      staticSecretsDirectory = Nothing,
      workDirectory = Nothing,
      clusterJoinTokenPath = Nothing,
      binaryCachesPath = Nothing,
      secretsJsonPath = Nothing,
      logLevel = Nothing,
      nixVerbosity = Nothing,
      labels = Nothing,
      allowInsecureBuiltinFetchers = Nothing,
      remotePlatformsWithSameFeatures = Nothing,
      effectMountables = mempty,
      nixSettings = mempty
    }