Group configuration by component ownership

This commit is contained in:
ms2k 2026-07-20 02:21:33 +09:00
commit 67cc0cb802

View file

@ -4,7 +4,7 @@ import Control.Concurrent (threadDelay)
import Control.Exception (SomeException, try)
import Control.Monad (unless)
import Data.Foldable (traverse_)
import Nuthatch.Config (Config (..), loadConfig)
import Nuthatch.Config
import Nuthatch.Redis
import Nuthatch.Types
import System.Environment
@ -20,9 +20,9 @@ main = do
case mode of
["config"] -> verifyConfiguration
["available"] -> withConfiguration $ \config ->
withRedis config (verifyAvailable config)
withRedis (cfgRedis config) (verifyAvailable config)
["unavailable"] -> withConfiguration $ \config ->
withRedis config (verifyUnavailable config)
withRedis (cfgRedis config) (verifyUnavailable config)
_ -> fail "expected config, available, or unavailable mode"
withConfiguration :: (Config -> IO ()) -> IO ()
@ -41,12 +41,47 @@ verifyConfiguration = do
, "NUTHATCH_BROWSE_PAGE_SIZE"
, "NUTHATCH_PUBLIC_BASE_URL"
, "NUTHATCH_REDIS_URL"
, "NUTHATCH_LISTEN"
, "NUTHATCH_SESSION_HOURS"
, "NUTHATCH_OIDC_ADDITIONAL_AUDIENCES"
, "NUTHATCH_OIDC_MAX_ID_TOKEN_SECONDS"
, "NUTHATCH_S3_REGION"
, "NUTHATCH_PERSONAL_QUOTA_BYTES"
, "NUTHATCH_MAX_OBJECT_BYTES"
, "NUTHATCH_THROTTLE_BYTES_PER_SEC"
, "NUTHATCH_RECOVERY_GRACE_SEC"
, "NUTHATCH_OBJECT_REQUEST_TIMEOUT_SEC"
, "NUTHATCH_STARTUP_ATTEMPTS"
, "NUTHATCH_STARTUP_DELAY_MS"
]
let load changes = do
traverse_ (uncurry setVariable) baseValues
traverse_ (uncurry setVariable) changes
loadConfig
expectAccepted (load []) "valid configuration"
grouped <- load
[ ("NUTHATCH_LISTEN", Just "127.0.0.2:9090")
, ("NUTHATCH_SESSION_HOURS", Just "6")
, ("NUTHATCH_OIDC_MAX_ID_TOKEN_SECONDS", Just "1800")
, ("NUTHATCH_S3_REGION", Just "group-test")
, ("NUTHATCH_PERSONAL_QUOTA_BYTES", Just "2048")
, ("NUTHATCH_THROTTLE_BYTES_PER_SEC", Just "4096")
, ("NUTHATCH_RECOVERY_GRACE_SEC", Just "45")
, ("NUTHATCH_OBJECT_REQUEST_TIMEOUT_SEC", Just "120")
, ("NUTHATCH_STARTUP_ATTEMPTS", Just "7")
, ("NUTHATCH_STARTUP_DELAY_MS", Just "25")
]
requireEqual "127.0.0.2" (cfgListenHost (cfgHttp grouped))
requireEqual 6 (cfgSessionHours (cfgHttp grouped))
requireEqual 1800 (cfgOidcMaxIdTokenSeconds (cfgOidc grouped))
requireEqual "group-test" (cfgS3Region (cfgS3 grouped))
requireEqual 2048 (cfgPersonalQuotaBytes (cfgProduct grouped))
requireEqual 4096 (cfgThrottleBytesPerSec (cfgRedis grouped))
requireEqual 45 (cfgRecoveryGraceSec (cfgRecovery grouped))
requireEqual 120 (cfgObjectRequestTimeoutSec (cfgRecovery grouped))
requireEqual 120 (cfgS3RequestTimeoutSec (cfgS3 grouped))
requireEqual 7 (cfgStartupAttempts (cfgRuntime grouped))
requireEqual 25 (cfgStartupDelayMs (cfgRuntime grouped))
expectRejected
(load [("NUTHATCH_PG_CONN", Nothing)])
"missing required configuration"
@ -60,6 +95,30 @@ verifyConfiguration = do
(load
[("NUTHATCH_BROWSE_PAGE_SIZE", Just "18446744073709551617")])
"out-of-range configuration"
expectRejected
(load [("NUTHATCH_LISTEN", Just "127.0.0.1:0")])
"invalid HTTP listener configuration"
expectRejected
(load [("NUTHATCH_JWT_SECRET", Just "too-short")])
"short session secret"
expectRejected
(load [("NUTHATCH_OIDC_ADDITIONAL_AUDIENCES", Just "extra,extra")])
"duplicate OIDC audiences"
expectRejected
(load [("NUTHATCH_REDIS_URL", Just "redis://user:secret@redis:6379/0")])
"credential-bearing Redis configuration"
expectRejected
(load [("NUTHATCH_MAX_OBJECT_BYTES", Just "999999999999999")])
"unsupported product object limit"
expectRejected
(load [("NUTHATCH_THROTTLE_BYTES_PER_SEC", Just "0")])
"invalid Redis throttle configuration"
expectRejected
(load [("NUTHATCH_RECOVERY_GRACE_SEC", Just "0")])
"invalid recovery configuration"
expectRejected
(load [("NUTHATCH_STARTUP_ATTEMPTS", Just "0")])
"invalid runtime startup configuration"
expectRejected
(load
[ ( "NUTHATCH_PUBLIC_BASE_URL"
@ -91,9 +150,13 @@ setVariable name = maybe (unsetEnv name) (setEnv name)
verifyAvailable :: Config -> Redis -> IO ()
verifyAvailable config redis = do
cacheSet redis "nuthatch:runtime:ttl" (cfgListCacheTtlSec config) "cached"
let productCfg = cfgProduct config
cacheSet redis "nuthatch:runtime:ttl"
(cfgListCacheTtlSec productCfg)
"cached"
cacheGet redis "nuthatch:runtime:ttl" >>= requireEqual (Just "cached")
threadDelay (fromInteger ((cfgListCacheTtlSec config + 1) * 1000000))
threadDelay
(fromInteger ((cfgListCacheTtlSec productCfg + 1) * 1000000))
cacheGet redis "nuthatch:runtime:ttl" >>= requireEqual Nothing
let userA = UserId "runtime-user-a"
userB = UserId "runtime-user-b"
@ -115,7 +178,11 @@ verifyUnavailable config redis = do
cacheGet redis "nuthatch:runtime:missing" >>= requireEqual Nothing
cacheSet redis "nuthatch:runtime:missing" 1 "ignored"
cacheInvalidatePrefixes redis (UserId "runtime-user") Shared [""]
throttled <- throttleCheck redis config (UserId "runtime-user") 1
throttled <- throttleCheck
redis
(cfgRedis config)
(UserId "runtime-user")
1
case throttled of
Left _ -> pure ()
Right _ -> fail "transfer throttle did not fail closed"