sandwich-contexts-kubernetes-0.1.1.1: lib/Test/Sandwich/Contexts/Kubernetes/KataContainers.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
{-|
Install [Kata Containers](https://katacontainers.io) on a Kubernetes cluster.
-}
module Test.Sandwich.Contexts.Kubernetes.KataContainers (
-- * Introduce Kata Containers
introduceKataContainers
-- * Bracket-style versions
, withKataContainers
, withKataContainers'
-- * Types
, KataContainersOptions(..)
, SourceCheckout(..)
, defaultKataContainersOptions
, kataContainers
, KataContainersContext(..)
, HasKataContainersContext
) where
import Control.Lens
import Control.Monad
import Control.Monad.IO.Unlift
import Data.Aeson (FromJSON)
import Data.String.Interpolate
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Data.Yaml as Yaml
import Kubernetes.OpenAPI.Model as Kubernetes
import Kubernetes.OpenAPI.ModelLens as Kubernetes
import Relude hiding (withFile)
import Safe
import System.Exit
import System.FilePath
import Test.Sandwich
import Test.Sandwich.Contexts.Files
import Test.Sandwich.Contexts.Kubernetes.FindImages
import Test.Sandwich.Contexts.Kubernetes.Images
import Test.Sandwich.Contexts.Kubernetes.Kubectl
import Test.Sandwich.Contexts.Kubernetes.Types
import Test.Sandwich.Contexts.Nix
import Test.Sandwich.Waits
import UnliftIO.Process
data KataContainersContext = KataContainersContext {
kataContainersOptions :: KataContainersOptions
} deriving (Show)
data SourceCheckout =
SourceCheckoutFilePath FilePath
| SourceCheckoutNixDerivation Text
deriving (Show, Eq)
data KataContainersOptions = KataContainersOptions {
kataContainersSourceCheckout :: SourceCheckout
-- | If set, this will overwrite the image in the DaemonSet in @kata-deploy.yaml@ and will set the 'ImagePullPolicy'
-- to 'IfNotPresent'.
-- This is useful because it's currently (8\/15\/2024) set to @quay.io\/kata-containers\/kata-deploy:latest@,
-- with @imagePullPolicy: Always@. This is not reproducible and also doesn't allow us to cache images.
, kataContainersKataDeployImage :: Maybe Text
-- | Whether to pull the image using Docker and load it onto the cluster using 'loadImageIfNecessary''.
, kataContainersPreloadImages :: Bool
-- | Whether to label the node(s) with @katacontainers.io/kata-runtime=true@, since this seems not to happen
-- automatically with kata-deploy.
, kataContainersLabelNode :: Bool
} deriving (Show)
defaultKataContainersOptions :: KataContainersOptions
defaultKataContainersOptions = KataContainersOptions {
kataContainersSourceCheckout = SourceCheckoutNixDerivation kataContainersDerivation
, kataContainersKataDeployImage = Just kataContainersDeployImage
, kataContainersPreloadImages = True
, kataContainersLabelNode = True
}
kataContainers :: Label "kataContainers" KataContainersContext
kataContainers = Label
type HasKataContainersContext context = HasLabel context "kataContainers" KataContainersContext
type ContextWithKataContainers context =
LabelValue "kataContainers" KataContainersContext
:> LabelValue "file-kubectl" (EnvironmentFile "kubectl")
:> context
-- | Install Kata Containers on the cluster and introduce a 'KataContainersContext'.
introduceKataContainers :: (
Typeable context, KubernetesClusterBasicWithoutReader context m, HasNixContext context
)
-- | Options
=> KataContainersOptions
-> SpecFree (ContextWithKataContainers context) m ()
-> SpecFree context m ()
introduceKataContainers options = introduceBinaryViaNixPackage @"kubectl" "kubectl" . introduceWith "introduce KataContainers" kataContainers (void . withKataContainers options)
-- | Bracket-style version of 'introduceKataContainers'.
withKataContainers :: forall context m a. (
HasCallStack, Typeable context, MonadFail m, KubectlBasic context m
)
-- | Options
=> KataContainersOptions
-> (KataContainersContext -> m a)
-> m a
withKataContainers options action = do
kcc <- getContext kubernetesCluster
kubectlBinary <- askFile @"kubectl"
withKataContainers' kcc kubectlBinary options action
-- | 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, KubernetesBasic context m
)
=> KubernetesClusterContext
-- | Path to @kubectl@ binary
-> FilePath
-> KataContainersOptions
-> (KataContainersContext -> m a)
-> m a
withKataContainers' kcc@(KubernetesClusterContext {..}) kubectlBinary options@(KataContainersOptions {..}) action = do
-- Preflight checks
case kubernetesClusterType of
KubernetesClusterKind {} -> expectationFailure [i|Can't install Kata Containers on Kind at present.|]
KubernetesClusterMinikube {..} -> do
output <- readCreateProcessWithLogging (proc kubernetesClusterTypeMinikubeBinary [
"--profile", toString kubernetesClusterTypeMinikubeProfileName
, "ssh", [i|egrep -c 'vmx|svm' /proc/cpuinfo|]
]) ""
case readMay output of
Just (0 :: Int) -> expectationFailure [i|Preflight check: didn't find "vmx" or "svm" in /proc/cpuinfo. Please make sure virtualization support is enabled.|]
Just _ -> return ()
Nothing -> expectationFailure [i|Preflight check: couldn't parse output of minikube ssh "egrep -c 'vmx|svm' /proc/cpuinfo"|]
-- Get Kata source dir
kataRoot <- case kataContainersSourceCheckout of
SourceCheckoutFilePath x -> pure x
SourceCheckoutNixDerivation d -> getContextMaybe nixContext >>= \case
Nothing -> expectationFailure [i|Wanted to build Kata Containers source checkout via derivation, but no Nix context was provided.|]
Just nc -> buildNixCallPackageDerivation' nc d
info [i|kataRoot: #{kataRoot}|]
env <- askKubectlEnvironment kcc
-- Now follow the instructions from
-- https://github.com/kata-containers/kata-containers/blob/main/docs/install/minikube-installation-guide.md#installing-kata-containers
-- Read the RBAC and DaemonSet configs
rbacContents <- liftIO $ T.readFile $ kataRoot </> "tools/packaging/kata-deploy/kata-rbac/base/kata-rbac.yaml"
deploymentContents' <- liftIO $ T.readFile $ kataRoot </> "tools/packaging/kata-deploy/kata-deploy/base/kata-deploy.yaml"
let deploymentContents = case kataContainersKataDeployImage of
Nothing -> deploymentContents'
Just deployImage -> deploymentContents'
& setDaemonSetImage deployImage
-- Preload any images
when kataContainersPreloadImages $ do
let images = findAllImages (rbacContents <> "\n---\n" <> deploymentContents)
forM_ images $ \image -> do
info [i|Preloading image: #{image}|]
loadImageIfNecessary' kcc (ImageLoadSpecDocker image IfNotPresent)
-- Install kata-deploy
debug [i|Applying kata-rbac.yaml|]
createProcessWithLoggingAndStdin ((proc kubectlBinary ["apply", "-f", "-"]) { env = Just env }) (toString rbacContents)
>>= waitForProcess >>= (`shouldBe` ExitSuccess)
debug [i|Applying kata-deploy.yaml|]
createProcessWithLoggingAndStdin ((proc kubectlBinary ["apply", "-f", "-"]) { env = Just env }) (toString deploymentContents)
>>= waitForProcess >>= (`shouldBe` ExitSuccess)
debug [i|Waiting for kata-deploy pod to exist|]
podName <- waitUntil 600 $ do
pods <- (T.words . toText) <$> readCreateProcessWithLogging ((
(proc kubectlBinary ["-n", "kube-system"
, "get", "pods", "-o=name"]) { env = Just env }
) { env = Just env }) ""
case headMay [t | t <- pods, "pod/kata-deploy" `T.isPrefixOf` t] of
Just x -> pure x
Nothing -> expectationFailure [i|Couldn't find kata-deploy pod in: #{pods}|]
info [i|Got podName: #{podName}|]
-- Wait until the kata-deploy pod starts sleeping
waitUntil 600 $ do
(exitCode, sout, serr) <- readCreateProcessWithExitCode (
(proc kubectlBinary ["-n", "kube-system"
, "exec", toString podName
, "--"
, "ps", "-ef"
])
{ env = Just env }
) ""
case exitCode of
ExitSuccess -> return ()
ExitFailure n -> expectationFailure [i|Command failed with code #{n}. Stderr: #{serr}|]
toText sout `textShouldContain` "sleep infinity"
-- Now install the runtime classes
debug [i|Applying kata-runtimeClasses.yaml|]
runtimeClassesContents <- liftIO $ T.readFile $ kataRoot </> "tools/packaging/kata-deploy/runtimeclasses/kata-runtimeClasses.yaml"
createProcessWithLoggingAndStdin ((proc kubectlBinary ["apply", "-f", "-"]) { env = Just env }) (toString runtimeClassesContents)
>>= waitForProcess >>= (`shouldBe` ExitSuccess)
-- Finally, label the node(s)
debug [i|Labeling nodes with katacontainers.io/kata-runtime=true|]
when kataContainersLabelNode $ do
createProcessWithLoggingAndStdin ((proc kubectlBinary ["label", "nodes", "--all", "--overwrite", "katacontainers.io/kata-runtime=true"]) { env = Just env }) (toString deploymentContents)
>>= waitForProcess >>= (`shouldBe` ExitSuccess)
action $ KataContainersContext options
kataContainersDeployImage :: Text
kataContainersDeployImage = "quay.io/kata-containers/kata-deploy:3.19.1"
-- | Checkout of the @kata-containers@ repo. Currently at release 3.19.1.
kataContainersDerivation :: Text
kataContainersDerivation = [__i|{fetchFromGitHub}:
fetchFromGitHub {
owner = "kata-containers";
repo = "kata-containers";
rev = "acae4480ac84701d7354e679714cc9d084b37f44";
sha256 = "sha256-h9Jsto2l1NhQEwIQoecT/D+yt/QbGoqqH/l6NNzJOwk=";
}
|]
setDaemonSetImage :: Text -> Text -> Text
setDaemonSetImage image = mconcat . fmap setDaemonSetImage' . T.splitOn "---\n"
where
setDaemonSetImage' :: Text -> Text
setDaemonSetImage' (decode -> Right x@(V1DaemonSet {v1DaemonSetKind=(Just "DaemonSet")})) = x
& set (v1DaemonSetSpecL . _Just . v1DaemonSetSpecTemplateL . v1PodTemplateSpecSpecL . _Just . v1PodSpecContainersL . ix 0 . v1ContainerImageL) (Just image)
& set (v1DaemonSetSpecL . _Just . v1DaemonSetSpecTemplateL . v1PodTemplateSpecSpecL . _Just . v1PodSpecContainersL . ix 0 . v1ContainerImagePullPolicyL) (Just "IfNotPresent")
& Yaml.encode
& decodeUtf8
setDaemonSetImage' t = t
decode :: FromJSON a => Text -> Either Yaml.ParseException a
decode = Yaml.decodeEither' . encodeUtf8