module Main (main) where import Control.Concurrent (threadDelay) import Control.Exception (bracket) import Control.Monad (unless) import qualified Data.ByteString.Char8 as B8 import Database.PostgreSQL.Simple ( Only (..) , close , connectPostgreSQL , execute_ , query_ ) import Database.Redis ( ConnectInfo (..) , PortID (..) , Reply , Status , connect , defaultConnectInfo , disconnect , get , runRedis , set ) import Nuthatch.Probe.Assert import Nuthatch.Probe.Curl main :: IO () main = do verifyPostgres verifyCoordination "redis" verifyCoordination "dragonfly" verifyOidc verifyTarget "Garage" "http://garage:3900" "garage" "GK00000000000000000000000000000000" "0000000000000000000000000000000000000000000000000000000000000000" False verifyTarget "VersityGW" "http://versitygw:10000" "us-east-1" "synthetic-s3-access" "synthetic-s3-secret" True putStrLn "Dependency product interfaces passed" verifyPostgres :: IO () verifyPostgres = bracket (connectPostgreSQL "host=postgres port=5432 user=nuthatch password=synthetic-postgres dbname=nuthatch") close (\connection -> do _ <- execute_ connection "CREATE TEMP TABLE dependency_probe (value text NOT NULL)" _ <- execute_ connection "INSERT INTO dependency_probe VALUES ('passed')" values <- query_ connection "SELECT value FROM dependency_probe" :: IO [Only B8.ByteString] assertEqual "PostgreSQL product interface" [Only "passed"] values) verifyCoordination :: String -> IO () verifyCoordination host = bracket (connect defaultConnectInfo { connectHost = host , connectPort = PortNumber 6379 }) disconnect (\connection -> do stored <- runRedis connection (set "nuthatch-probe" "passed") :: IO (Either Reply Status) assert "coordination write failed" (isRight stored) loaded <- runRedis connection (get "nuthatch-probe") :: IO (Either Reply (Maybe B8.ByteString)) assertEqual (host <> " product interface") (Right (Just "passed")) loaded) verifyOidc :: IO () verifyOidc = mapM_ (\(label, url, marker) -> retry label 30 $ do response <- curl [url] pure ( curlStatus response == 200 && marker `B8.isInfixOf` curlBody response )) [ ( "Keycloak discovery" , "http://keycloak.nuthatch.test:8080/realms/master/.well-known/openid-configuration" , "\"issuer\"" ) , ( "Keycloak keys" , "http://keycloak.nuthatch.test:8080/realms/master/protocol/openid-connect/certs" , "\"keys\"" ) , ( "ZITADEL discovery" , "http://zitadel.nuthatch.test:8080/.well-known/openid-configuration" , "\"issuer\"" ) , ( "ZITADEL keys" , "http://zitadel.nuthatch.test:8080/oauth/v2/keys" , "\"keys\"" ) ] verifyTarget :: String -> String -> String -> String -> String -> Bool -> IO () verifyTarget label endpoint region accessKey secretKey createBucket = do let signed = [ "--aws-sigv4", "aws:amz:" <> region <> ":s3" , "--user", accessKey <> ":" <> secretKey ] bucket = endpoint <> "/nuthatch-fixture" payload = "nuthatch-dependency-compatibility-probe" payloadDigest = "d0d4c515c2723784b6ca1b22b97d89ae8229a9c2abcb9ca912b85c1f02e17303" emptyDigest = "e3b0c44298fc1c149afbf4c8996fb924" <> "27ae41e4649b934ca495991b7852b855" if createBucket then do created <- curl ( signed <> [ "--request", "PUT" , "--header", "X-Amz-Content-Sha256:" <> emptyDigest , bucket ] ) assert ( label <> " bucket creation failed: " <> B8.unpack (curlBody created) ) $ curlStatus created `elem` [200, 409] else pure () uploaded <- retryResponse 30 $ curl ( signed <> [ "--request", "PUT" , "--header" , "X-Amz-Content-Sha256:" <> payloadDigest , "--data-binary", payload , bucket <> "/probe/payload" ] ) assertEqual (label <> " upload: " <> B8.unpack (curlBody uploaded)) 200 (curlStatus uploaded) downloaded <- curl ( signed <> [ "--header" , "X-Amz-Content-Sha256:" <> emptyDigest , bucket <> "/probe/payload" ] ) assertEqual (label <> " download status") 200 (curlStatus downloaded) assertEqual (label <> " object body") (B8.pack payload) (curlBody downloaded) retry :: String -> Int -> IO Bool -> IO () retry label attempts action = go attempts where go remaining = do succeeded <- action unless succeeded $ if remaining <= 1 then fail (label <> " did not become available") else threadDelay 1000000 >> go (remaining - 1) retryResponse :: Int -> IO CurlResponse -> IO CurlResponse retryResponse attempts action = go attempts where go remaining = do response <- action if curlStatus response == 200 || remaining <= 1 then pure response else threadDelay 1000000 >> go (remaining - 1) isRight :: Either left right -> Bool isRight (Right _) = True isRight (Left _) = False