sandwich-contexts-kubernetes 0.1.0.0 → 0.1.1.0
raw patch · 14 files changed
+89/−82 lines, 14 filesdep +kubernetes-apidep +kubernetes-api-clientdep −kubernetes-clientdep −kubernetes-client-core
Dependencies added: kubernetes-api, kubernetes-api-client
Dependencies removed: kubernetes-client, kubernetes-client-core
Files
- lib/Test/Sandwich/Contexts/Kubernetes.hs +2/−3
- lib/Test/Sandwich/Contexts/Kubernetes/KataContainers.hs +3/−4
- lib/Test/Sandwich/Contexts/Kubernetes/KindCluster.hs +5/−6
- lib/Test/Sandwich/Contexts/Kubernetes/KindCluster/ServiceForwardPortForward.hs +2/−3
- lib/Test/Sandwich/Contexts/Kubernetes/KindCluster/Setup.hs +1/−2
- lib/Test/Sandwich/Contexts/Kubernetes/KubectlPortForward.hs +2/−4
- lib/Test/Sandwich/Contexts/Kubernetes/MinioS3Server.hs +26/−13
- lib/Test/Sandwich/Contexts/Kubernetes/Run.hs +4/−6
- lib/Test/Sandwich/Contexts/Kubernetes/SeaweedFS.hs +7/−4
- lib/Test/Sandwich/Contexts/Kubernetes/Util/Nix.hs +3/−3
- lib/Test/Sandwich/Contexts/Kubernetes/Util/Ports.hs +21/−19
- lib/Test/Sandwich/Contexts/Kubernetes/Waits.hs +7/−8
- sandwich-contexts-kubernetes.cabal +5/−5
- test/Spec.hs +1/−2
lib/Test/Sandwich/Contexts/Kubernetes.hs view
@@ -63,7 +63,6 @@ , KubectlBasicWithoutReader ) where -import Control.Monad.Catch import Control.Monad.IO.Unlift import Control.Monad.Logger import Network.URI@@ -85,7 +84,7 @@ -- | Forward a Kubernetes service, so that it can be reached at a local URI. withForwardKubernetesService :: (- MonadMask m, KubectlBasic context m+ KubectlBasic context m ) -- | Namespace => Text@@ -101,7 +100,7 @@ -- | Same as 'withForwardKubernetesService', but allows you to pass in the 'KubernetesClusterContext' and @kubectl@ binary. withForwardKubernetesService' :: (- MonadLoggerIO m, MonadMask m, MonadUnliftIO m+ MonadLoggerIO m, MonadUnliftIO m , HasBaseContextMonad context m ) -- | Kubernetes cluster context
lib/Test/Sandwich/Contexts/Kubernetes/KataContainers.hs view
@@ -33,7 +33,6 @@ import Control.Lens import Control.Monad-import Control.Monad.Catch (MonadMask) import Control.Monad.IO.Unlift import Data.Aeson (FromJSON) import Data.String.Interpolate@@ -98,7 +97,7 @@ -- | Install Kata Containers on the cluster and introduce a 'KataContainersContext'. introduceKataContainers :: (- MonadMask m, Typeable context, KubernetesClusterBasicWithoutReader context m, HasNixContext context+ Typeable context, KubernetesClusterBasicWithoutReader context m, HasNixContext context ) -- | Options => KataContainersOptions@@ -108,7 +107,7 @@ -- | Bracket-style version of 'introduceKataContainers'. withKataContainers :: forall context m a. (- HasCallStack, Typeable context, MonadFail m, MonadMask m, KubectlBasic context m+ HasCallStack, Typeable context, MonadFail m, KubectlBasic context m ) -- | Options => KataContainersOptions@@ -121,7 +120,7 @@ -- | Same as 'withKataContainers', but allows you to pass in the 'KubernetesClusterContext' and @kubectl@ binary path. withKataContainers' :: forall context m a. (- HasCallStack, Typeable context, MonadFail m, MonadMask m, KubernetesBasic context m+ HasCallStack, Typeable context, MonadFail m, KubernetesBasic context m ) => KubernetesClusterContext -- | Path to @kubectl@ binary
lib/Test/Sandwich/Contexts/Kubernetes/KindCluster.hs view
@@ -40,7 +40,6 @@ ) where import Control.Monad-import Control.Monad.Catch (MonadMask) import Control.Monad.IO.Unlift import Control.Monad.Logger import Data.String.Interpolate@@ -116,7 +115,7 @@ -- | Introduce a Kubernetes cluster using [kind](https://kind.sigs.k8s.io/), deriving the @kind@ and @kubectl@ binaries from the Nix context. introduceKindClusterViaNix :: (- HasBaseContext context, MonadUnliftIO m, MonadMask m, HasNixContext context+ HasBaseContext context, MonadUnliftIO m, HasNixContext context ) -- | Options => KindClusterOptions@@ -131,7 +130,7 @@ -- | Introduce a Kubernetes cluster using [kind](https://kind.sigs.k8s.io/), deriving the @kind@ and @kubectl@ binaries from the PATH. introduceKindClusterViaEnvironment :: (- HasBaseContext context, MonadMask m, MonadUnliftIO m+ HasBaseContext context, MonadUnliftIO m ) -- | Options => KindClusterOptions@@ -144,7 +143,7 @@ -- | Introduce a Kubernetes cluster using [kind](https://kind.sigs.k8s.io/), passing in the @kind@ and @kubectl@ binaries. introduceKindCluster' :: (- HasBaseContext context, MonadMask m, MonadUnliftIO m+ HasBaseContext context, MonadUnliftIO m ) -- | Path to kind binary => FilePath@@ -163,7 +162,7 @@ -- | Bracket-style variant of 'introduceKindCluster''. withKindCluster :: (- MonadLoggerIO m, MonadUnliftIO m, MonadMask m, MonadFail m+ MonadLoggerIO m, MonadUnliftIO m, MonadFail m , HasBaseContextMonad context m, HasFile context "kind", HasFile context "kubectl" ) -- | Options@@ -177,7 +176,7 @@ -- | Same as 'withKindCluster', but allows you to pass in the paths to the @kind@ and @kubectl@ binaries. withKindCluster' :: (- MonadLoggerIO m, MonadUnliftIO m, MonadMask m, MonadFail m+ MonadLoggerIO m, MonadUnliftIO m, MonadFail m , HasBaseContextMonad context m ) -- | Path to the kind binary
lib/Test/Sandwich/Contexts/Kubernetes/KindCluster/ServiceForwardPortForward.hs view
@@ -6,12 +6,11 @@ module Test.Sandwich.Contexts.Kubernetes.KindCluster.ServiceForwardPortForward where -import Control.Monad.Catch (MonadCatch) import Control.Monad.IO.Unlift import Control.Monad.Logger import qualified Data.List as L import Data.String.Interpolate-import Data.Text as T+import qualified Data.Text as T import Network.URI import Relude hiding (withFile) import Safe@@ -23,7 +22,7 @@ withForwardKubernetesService' :: (- MonadUnliftIO m, MonadCatch m, MonadLoggerIO m+ MonadUnliftIO m, MonadLoggerIO m , HasBaseContextMonad context m ) => KubernetesClusterContext -> FilePath -> Text -> Text -> (URI -> m a) -> m a withForwardKubernetesService' (KubernetesClusterContext {kubernetesClusterType=(KubernetesClusterKind {..}), ..}) kubectlBinary namespace service action = do
lib/Test/Sandwich/Contexts/Kubernetes/KindCluster/Setup.hs view
@@ -10,7 +10,6 @@ ) where import Control.Monad-import Control.Monad.Catch ( MonadMask) import Control.Monad.IO.Unlift import Control.Monad.Logger import qualified Data.List as L@@ -26,7 +25,7 @@ setUpKindCluster :: (- MonadLoggerIO m, MonadUnliftIO m, MonadMask m+ MonadLoggerIO m, MonadUnliftIO m ) => KubernetesClusterContext -> FilePath -> FilePath -> Maybe [(String, String)] -> Text -> m () setUpKindCluster kcc@(KubernetesClusterContext {..}) kindBinary kubectlBinary environmentToUse driver = do baseEnv <- maybe getEnvironment return environmentToUse
lib/Test/Sandwich/Contexts/Kubernetes/KubectlPortForward.hs view
@@ -8,7 +8,6 @@ ) where import Control.Monad-import Control.Monad.Catch (MonadCatch) import Control.Monad.IO.Unlift import Control.Retry import Data.String.Interpolate@@ -43,7 +42,7 @@ -- Note that this will stop working if the pod you're talking to goes away (even if you do it against a service). -- If this happens, a rerun of the command is needed to resume port forwarding. withKubectlPortForward :: (- HasCallStack, MonadCatch m, KubectlBasic context m+ HasCallStack, KubectlBasic context m ) -- | Path to kubeconfig file => FilePath@@ -61,7 +60,7 @@ -- | Same as 'withKubectlPortForward', but allows you to pass in the @kubectl@ binary path. withKubectlPortForward' :: (- HasCallStack, MonadCatch m, KubernetesBasic context m+ HasCallStack, KubernetesBasic context m ) => FilePath -- | Path to kubeconfig file@@ -94,7 +93,6 @@ let logPath = dir </> toString (T.replace "/" "_" targetName) <.> "port-forwarding.log" withFile logPath WriteMode $ \h -> do- let restarterThread = forever $ do bracket (createProcess ((proc kubectlBinary args) { std_out = UseHandle h
lib/Test/Sandwich/Contexts/Kubernetes/MinioS3Server.hs view
@@ -21,6 +21,7 @@ -- * Types , MinioS3ServerOptions(..) , defaultMinioS3ServerOptions+ , NetworkPolicies(..) -- * Re-exports , testS3Server@@ -29,7 +30,6 @@ ) where import Control.Monad-import Control.Monad.Catch (MonadMask) import Control.Monad.IO.Unlift import Data.String.Interpolate import Data.Text as T@@ -56,14 +56,22 @@ minioS3ServerNamespace :: Text , minioS3ServerKustomizationDir :: KustomizationDir , minioS3ServerPreloadImages :: Bool+ , minioS3ServerNetworkPolicies :: Maybe NetworkPolicies } defaultMinioS3ServerOptions :: Text -> MinioS3ServerOptions defaultMinioS3ServerOptions namespace = MinioS3ServerOptions { minioS3ServerNamespace = namespace , minioS3ServerKustomizationDir = KustomizationDirUrl "https://github.com/minio/operator/examples/kustomization/base?ref=v6.0.1" , minioS3ServerPreloadImages = True+ , minioS3ServerNetworkPolicies = Nothing } +data NetworkPolicies = NetworkPolicies {+ networkPoliciesNames :: [String]+ , networkPoliciesAllYaml :: String+ }+ deriving (Show, Eq)+ data KustomizationDir = -- | URL Kustomize dir to be downloaded KustomizationDirUrl Text@@ -76,7 +84,7 @@ -- | Introduce a MinIO server on a Kubernetes cluster. -- Must have a 'minioOperator' context. introduceK8SMinioS3Server :: (- MonadMask m, Typeable context, KubectlBasicWithoutReader context m, HasMinioOperatorContext context+ Typeable context, KubectlBasicWithoutReader context m, HasMinioOperatorContext context ) -- | Options => MinioS3ServerOptions@@ -90,7 +98,7 @@ -- | Same as 'introduceK8SMinioS3Server', but allows you to pass in the 'KubernetesClusterContext'. introduceK8SMinioS3Server' :: (- MonadMask m, Typeable context, KubectlBasic context m, HasMinioOperatorContext context+ Typeable context, KubectlBasic context m, HasMinioOperatorContext context ) => KubernetesClusterContext -- | Options@@ -104,7 +112,7 @@ -- | Bracket-style variant of 'introduceK8SMinioS3Server'. withK8SMinioS3Server :: (- Typeable context, MonadMask m, MonadFail m, KubernetesBasic context m, HasFile context "kubectl"+ Typeable context, MonadFail m, KubernetesBasic context m, HasFile context "kubectl" ) => KubernetesClusterContext -> MinioOperatorContext@@ -118,7 +126,7 @@ -- | Same as 'withK8SMinioS3Server', but allows you to pass in the @kubectl@ binary. withK8SMinioS3Server' :: forall m context. (- Typeable context, MonadMask m, MonadFail m, KubernetesBasic context m+ Typeable context, MonadFail m, KubernetesBasic context m ) -- | Path to kubectl binary => FilePath@@ -176,19 +184,19 @@ let destroy (_, finalYaml) = do info [i|-------------------------- DESTROYING --------------------------|]- createProcessWithLoggingAndStdin ((proc kubectlBinary ["apply", "-f", "-"]) { env = Just env }) (toString finalYaml)+ createProcessWithLoggingAndStdin ((proc kubectlBinary ["delete", "-f", "-"]) { env = Just env }) (toString finalYaml) >>= waitForProcess >>= (`shouldBe` ExitSuccess) -- Create network policy allowing ingress/egress for v1.min.io/tenant = deploymentName let createNetworkPolicy = do- let (policyName, discoverPodPolicyName, yaml) = networkPolicy deploymentName+ let NetworkPolicies policyNames yaml = fromMaybe (defaultNetworkPolicies deploymentName) minioS3ServerNetworkPolicies createProcessWithLoggingAndStdin ((proc kubectlBinary ["create", "--namespace", toString minioS3ServerNamespace, "-f", "-"]) { env = Just env, delegate_ctlc = True }) yaml >>= waitForProcess >>= (`shouldBe` ExitSuccess)- pure (policyName, discoverPodPolicyName)- let destroyNetworkPolicy (policyName, discoverPodPolicyName) = do- runWithKubeConfig kubectlBinary ["delete", "NetworkPolicy", policyName, "--namespace", toString minioS3ServerNamespace]- runWithKubeConfig kubectlBinary ["delete", "NetworkPolicy", discoverPodPolicyName, "--namespace", toString minioS3ServerNamespace]+ pure policyNames+ let destroyNetworkPolicy policyNames = do+ forM_ policyNames $ \name ->+ runWithKubeConfig kubectlBinary ["delete", "NetworkPolicy", name, "--namespace", toString minioS3ServerNamespace] bracket createNetworkPolicy destroyNetworkPolicy $ \_ -> bracket create destroy $ \((username, password), _) -> do do@@ -237,9 +245,14 @@ void $ action testServ -networkPolicy :: Text -> (String, String, String)-networkPolicy deploymentName = (policyName, discoverPodPolicyName, yaml)+defaultNetworkPolicies :: Text -> NetworkPolicies+defaultNetworkPolicies deploymentName = NetworkPolicies allPolicyNames yaml where+ allPolicyNames = [+ policyName+ , discoverPodPolicyName+ ]+ policyName = "minio-allow" discoverPodPolicyName = "discover-pod-allow"
lib/Test/Sandwich/Contexts/Kubernetes/Run.hs view
@@ -1,29 +1,27 @@-{-# LANGUAGE GADTs #-}-{-# LANGUAGE DataKinds #-} {-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE TypeOperators #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Test.Sandwich.Contexts.Kubernetes.Run where -import Test.Sandwich.Contexts.Kubernetes.Types import Control.Monad-import Control.Monad.Catch (MonadMask, MonadThrow) import Control.Monad.IO.Unlift import Control.Monad.Logger import qualified Data.ByteString.Lazy.Char8 as BL import Data.String.Interpolate-import Data.Text as T import Kubernetes.OpenAPI.Client as Kubernetes import Kubernetes.OpenAPI.Core as Kubernetes import Kubernetes.OpenAPI.MimeTypes import Network.HTTP.Client import Relude import Test.Sandwich+import Test.Sandwich.Contexts.Kubernetes.Types import UnliftIO.Exception -type Constraints context m = (MonadIO m, MonadThrow m, MonadUnliftIO m, MonadLogger m, MonadMask m, MonadReader context m)+type Constraints context m = (MonadIO m, MonadUnliftIO m, MonadLogger m, MonadReader context m) instance Exception MimeError
lib/Test/Sandwich/Contexts/Kubernetes/SeaweedFS.hs view
@@ -129,10 +129,13 @@ NixContext {..} <- getContext nixContext - let cp = proc nixContextNixBinary ["build", "--impure"- , "--extra-experimental-features", "nix-command"- , "--expr", seaweedFsOperatorDerivation- , "--json"]+ let cp = proc nixContextNixBinary [+ "build", "--impure"+ , "--extra-experimental-features", "nix-command"+ , "--expr", seaweedFsOperatorDerivation+ , "--no-link"+ , "--json"+ ] operatorJson <- withFile "/dev/null" WriteMode $ \hNull -> readCreateProcess (cp { std_err = UseHandle hNull }) ""
lib/Test/Sandwich/Contexts/Kubernetes/Util/Nix.hs view
@@ -3,18 +3,18 @@ withWritableBinaryCache ) where -import Control.Monad.Catch (MonadMask)+import Control.Monad.IO.Unlift import Control.Monad.Logger import Data.String.Interpolate import Relude import System.FilePath-import System.IO.Temp import Test.Sandwich.Logging import UnliftIO.Directory import UnliftIO.Process+import UnliftIO.Temporary -withWritableBinaryCache :: (MonadIO m, MonadMask m, MonadLogger m) => Maybe FilePath -> (Maybe FilePath -> m a) -> m a+withWritableBinaryCache :: (MonadUnliftIO m, MonadLogger m) => Maybe FilePath -> (Maybe FilePath -> m a) -> m a withWritableBinaryCache Nothing action = action Nothing withWritableBinaryCache (Just readOnlyPath) action = withSystemTempDirectory "writable-binary-cache" $ \dir -> do
lib/Test/Sandwich/Contexts/Kubernetes/Util/Ports.hs view
@@ -3,32 +3,34 @@ module Test.Sandwich.Contexts.Kubernetes.Util.Ports where -import Control.Monad.Catch (MonadCatch, catch)+import Control.Monad.IO.Unlift import Control.Retry import Network.Socket import Relude import System.Random (randomRIO)+import UnliftIO.Exception -- | Find an unused port in a given range findFreePortInRange' :: forall m. (- MonadIO m, MonadCatch m+ MonadUnliftIO m ) => RetryPolicy -> (PortNumber, PortNumber) -> [PortNumber] -> m (Maybe PortNumber) findFreePortInRange' retryPolicy (start, end) blacklist = retrying retryPolicy (\_retryStatus result -> return $ isNothing result) (const findFreePortInRange')- where getAcceptableCandidate :: m PortNumber- getAcceptableCandidate = do- candidate <- liftIO (fromInteger <$> randomRIO (fromIntegral start, fromIntegral end))- if | candidate `elem` blacklist -> getAcceptableCandidate- | otherwise -> return candidate+ where+ getAcceptableCandidate :: m PortNumber+ getAcceptableCandidate = do+ candidate <- liftIO (fromInteger <$> randomRIO (fromIntegral start, fromIntegral end))+ if | candidate `elem` blacklist -> getAcceptableCandidate+ | otherwise -> return candidate - findFreePortInRange' :: m (Maybe PortNumber)- findFreePortInRange' = do- candidate <- getAcceptableCandidate- isPortFree candidate >>= \case- False -> return Nothing- True -> return $ Just candidate+ findFreePortInRange' :: m (Maybe PortNumber)+ findFreePortInRange' = do+ candidate <- getAcceptableCandidate+ isPortFree candidate >>= \case+ False -> return Nothing+ True -> return $ Just candidate -isPortFree :: (MonadIO m, MonadCatch m) => PortNumber -> m Bool+isPortFree :: (MonadUnliftIO m) => PortNumber -> m Bool isPortFree candidate = catch (tryOpenAndClosePort candidate >> return True) (\(_ :: SomeException) -> return False) @@ -45,7 +47,7 @@ [] -> error "Couldn't resolve address 127.0.0.1" findFreePortInRange :: (- MonadIO m, MonadCatch m+ MonadUnliftIO m ) => (PortNumber, PortNumber) -> [PortNumber] -> m (Maybe PortNumber) findFreePortInRange = findFreePortInRange' (limitRetries 50) @@ -53,18 +55,18 @@ -- See https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers -- This works without a timeout since there should always be a port in there somewhere; -- it might be advisable to wrap in a timeout anyway.-findFreePort :: (MonadIO m, MonadCatch m) => m (Maybe PortNumber)+findFreePort :: (MonadUnliftIO m) => m (Maybe PortNumber) findFreePort = findFreePortInRange (49152, 65535) [] -findFreePortOrException :: (MonadIO m, MonadCatch m) => m PortNumber+findFreePortOrException :: (MonadUnliftIO m) => m PortNumber findFreePortOrException = findFreePortOrException' (const True) -findFreePortOrException' :: (MonadIO m, MonadCatch m) => (PortNumber -> Bool) -> m PortNumber+findFreePortOrException' :: (MonadUnliftIO m) => (PortNumber -> Bool) -> m PortNumber findFreePortOrException' isAcceptable = findFreePort >>= \case Just port | isAcceptable port -> return port | otherwise -> findFreePortOrException' isAcceptable Nothing -> error "Couldn't find free port" -findFreePortNotIn :: (MonadIO m, MonadCatch m) => [PortNumber] -> m (Maybe PortNumber)+findFreePortNotIn :: (MonadUnliftIO m) => [PortNumber] -> m (Maybe PortNumber) findFreePortNotIn = findFreePortInRange (49152, 65535)
lib/Test/Sandwich/Contexts/Kubernetes/Waits.hs view
@@ -1,18 +1,17 @@-{-# LANGUAGE GADTs #-}-{-# LANGUAGE DataKinds #-} {-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} module Test.Sandwich.Contexts.Kubernetes.Waits where -import Control.Monad.Catch (MonadMask) import Control.Monad.IO.Unlift import Control.Monad.Logger import qualified Data.List as L import qualified Data.Map as M import Data.String.Interpolate-import Data.Text as T+import qualified Data.Text as T import Kubernetes.OpenAPI.API.CoreV1 as Kubernetes import Kubernetes.OpenAPI.Core as Kubernetes import Kubernetes.OpenAPI.MimeTypes@@ -32,7 +31,7 @@ -- * They each have at least one IP address -- * They each have an empty set of "not ready addresses" waitForServiceEndpointsToExist :: (- MonadUnliftIO m, MonadLogger m, MonadMask m+ MonadUnliftIO m, MonadLogger m , MonadReader context m, HasKubernetesClusterContext context ) -- | Namespace@@ -66,7 +65,7 @@ listEndpoints :: (- MonadUnliftIO m, MonadLogger m, MonadMask m+ MonadUnliftIO m, MonadLogger m , MonadReader context m, HasKubernetesClusterContext context ) => Text -> Map Text Text -> m [V1Endpoints] listEndpoints namespace labels =@@ -77,7 +76,7 @@ -- | Wait for a set of pods to exist, specified by a set of labels. waitForPodsToExist :: (- MonadUnliftIO m, MonadLogger m, MonadMask m+ MonadUnliftIO m, MonadLogger m , MonadReader context m, HasKubernetesClusterContext context ) -- | Namespace@@ -98,7 +97,7 @@ -- | List the pods matching a set of labels. listPods :: (- MonadUnliftIO m, MonadLogger m, MonadMask m+ MonadUnliftIO m, MonadLogger m , MonadReader context m, HasKubernetesClusterContext context ) => Text -> Map Text Text -> m [V1Pod] listPods namespace labels =
sandwich-contexts-kubernetes.cabal view
@@ -1,16 +1,16 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.37.0.+-- This file has been generated from package.yaml by hpack version 0.38.0. -- -- see: https://github.com/sol/hpack name: sandwich-contexts-kubernetes-version: 0.1.0.0+version: 0.1.1.0 synopsis: Sandwich test contexts for Kubernetes description: Please see README.md author: Tom McLaughlin maintainer: tom@codedown.io-copyright: 2024 Tom McLaughlin+copyright: 2025 Tom McLaughlin license: BSD3 build-type: Simple @@ -76,8 +76,8 @@ , exceptions , filepath , http-client- , kubernetes-client- , kubernetes-client-core+ , kubernetes-api+ , kubernetes-api-client >=0.6.1.1 , lens , lens-aeson , minio-hs
test/Spec.hs view
@@ -1,7 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeApplications #-} -import Control.Monad.Catch (MonadMask) import Control.Monad.IO.Unlift import qualified Data.List as L import Data.String.Interpolate@@ -49,7 +48,7 @@ opts = defaultNodeOptions { nodeOptionsVisibilityThreshold = 50 } loadImageTests :: (- MonadUnliftIO m, MonadMask m+ MonadUnliftIO m , HasBaseContext context, HasKubernetesClusterContext context, HasNixContext context, HasFile context "kubectl" ) => SpecFree context m () loadImageTests = do