strawboss/src/main/nim/strawbosspkg/configuration.nim

206 lines
6.8 KiB
Nim

import logging, json, os, nre, sequtils, strtabs, tables, times
import private/util
from typeinfo import toAny
# Types
#
type
BuildStatus* = object
state*, details*: string
Step* = object
name*, stepCmd*, workingDir*: string
artifacts*, cmdInput*, depends*, expectedEnv*: seq[string]
dontSkip*: bool
ProjectConfig* = object
name*: string
versionCmd*: string
steps*: Table[string, Step]
ProjectDef* = object
cfgFilePath*, defaultBranch*, name*, repo*: string
envVars*: StringTableRef
RunRequest* = object
projectName*, stepName*, buildRef*, workspaceDir*: string
forceRebuild*: bool
User* = object
name*: string
hashedPwd*: string
UserRef* = ref User
StrawBossConfig* = object
artifactsRepo*: string
authSecret*: string
debug*: bool
projects*: seq[ProjectDef]
pwdCost*: int8
users*: seq[UserRef]
# Equality on custom types
proc `==`*(a, b: UserRef): bool = result = a.name == b.name
proc `==`*(a, b: ProjectDef): bool =
if a.envVars.len != b.envVars.len: return false
for k, v in a.envVars:
if not b.envVars.hasKey(k) or a.envVars[k] != b.envVars[k]: return false
return
a.name == b.name and
a.cfgFilePath == b.cfgFilePath and
a.defaultBranch == b.defaultBranch and
a.repo == b.repo
# Util methods on custom types
proc findProject*(cfg: StrawBossConfig, projectName: string): ProjectDef =
let candidates = cfg.projects.filterIt(it.name == projectName)
if candidates.len == 0:
raise newException(KeyError, "no project named " & projectName)
elif candidates.len > 0:
raise newException(KeyError, "multiple projects named " & projectName)
else: result = candidates[0]
# internal utils
let nullNode = newJNull()
proc getIfExists(n: JsonNode, key: string): JsonNode =
# convenience method to get a key from a JObject or return null
result = if n.hasKey(key): n[key]
else: nullNode
proc getOrFail(n: JsonNode, key: string, objName: string = ""): JsonNode =
# convenience method to get a key from a JObject or raise an exception
if not n.hasKey(key): raiseEx objName & " missing key '" & key & "'"
return n[key]
# Configuration parsing code
proc parseProjectDef*(pJson: JsonNode): ProjectDef =
var envVars = newStringTable(modeCaseSensitive)
for k, v in pJson.getIfExists("envVars").getFields: envVars[k] = v.getStr("")
result = ProjectDef(
cfgFilePath: pJson.getIfExists("cfgFilePath").getStr("strawboss.json"),
defaultBranch: pJson.getIfExists("defaultBranch").getStr("master"),
name: pJson.getOrFail("name", "project definition").getStr,
envVars: envVars,
repo: pJson.getOrFail("repo", "project definition").getStr)
proc loadStrawBossConfig*(cfgFile: string): StrawBossConfig =
if not existsFile(cfgFile):
raiseEx "strawboss config file not found: " & cfgFile
let jsonCfg = parseFile(cfgFile)
var users: seq[UserRef] = @[]
for uJson in jsonCfg.getIfExists("users").getElems:
users.add(UserRef(
name: uJson.getOrFail("name", "user record").getStr,
hashedPwd: uJson.getOrFail("hashedPwd", "user record").getStr))
result = StrawBossConfig(
artifactsRepo: jsonCfg.getIfExists("artifactsRepo").getStr("artifacts"),
authSecret: jsonCfg.getOrFail("authSecret", "strawboss config").getStr,
debug: jsonCfg.getIfExists("debug").getBVal(false),
pwdCost: int8(jsonCfg.getOrFail("pwdCost", "strawboss config").getNum),
projects: jsonCfg.getIfExists("projects").getElems.mapIt(parseProjectDef(it)),
users: users)
proc loadProjectConfig*(cfgFile: string): ProjectConfig =
if not existsFile(cfgFile):
raiseEx "project config file not found: " & cfgFile
let jsonCfg = parseFile(cfgFile)
if not jsonCfg.hasKey("steps"):
raiseEx "project configuration is missing steps definition"
var steps = initTable[string, Step]()
for sName, pJson in jsonCfg.getOrFail("steps", "project configuration").getFields:
steps[sName] = Step(
name: sName,
workingDir: pJson.getIfExists("workingDir").getStr("."),
stepCmd: pJson.getIfExists("stepCmd").getStr("sh"),
depends: pJson.getIfExists("depends").getElems.mapIt(it.getStr),
artifacts: pJson.getIfExists("artifacts").getElems.mapIt(it.getStr),
cmdInput: pJson.getIfExists("cmdInput").getElems.mapIt(it.getStr),
expectedEnv: pJson.getIfExists("expectedEnv").getElems.mapIt(it.getStr),
dontSkip: pJson.getIfExists("dontSkip").getBVal(false))
if steps[sName].stepCmd == "sh" and steps[sName].cmdInput.len == 0:
warn "Step " & sName & " uses 'sh' as its command but has no cmdInput."
result = ProjectConfig(
name: jsonCfg.getOrFail("name", "project configuration").getStr,
versionCmd: jsonCfg.getIfExists("versionCmd").getStr("git describe --tags --always"),
steps: steps)
proc loadBuildStatus*(statusFile: string): BuildStatus =
if not existsFile(statusFile): raiseEx "status file not found: " & statusFile
let jsonObj = parseFile(statusFile)
result = BuildStatus(
state: jsonObj.getOrFail("state", "build status").getStr,
details: jsonObj.getIfExists("details").getStr("") )
# TODO: unused and untested, add tests if we start using this
proc parseRunRequest*(reqStr: string): RunRequest =
let reqJson = parseJson(reqStr)
result = RunRequest(
projectName: reqJson.getOrFail("projectName", "RunRequest").getStr,
stepName: reqJson.getOrFail("stepName", "RunRequest").getStr,
buildRef: reqJson.getOrFail("buildRef", "RunRequest").getStr,
workspaceDir: reqJson.getOrFail("workspaceDir", "RunRequest").getStr,
forceRebuild: reqJson.getOrFail("forceRebuild", "RunRequest").getBVal)
# TODO: can we use the marshal module for this?
proc `%`*(s: BuildStatus): JsonNode =
result = %* {
"state": s.state,
"details": s.details
}
proc `%`*(p: ProjectDef): JsonNode =
result = %* {
"name": p.name,
"cfgFilePath": p.cfgFilePath,
"defaultBranch": p.defaultBranch,
"repo": p.repo }
result["envVars"] = newJObject()
for k, v in p.envVars: result["envVars"][k] = %v
proc `%`*(req: RunRequest): JsonNode =
result = %* {
"projectName": req.projectName,
"stepName": req.stepName,
"buildRef": req.buildRef,
"workspaceDir": req.workspaceDir,
"forceRebuild": req.forceRebuild }
proc `$`*(s: BuildStatus): string = result = pretty(%s)
proc `$`*(req: RunRequest): string = result = pretty(%req)
proc `$`*(pd: ProjectDef): string = result = pretty(%pd)
# TODO: maybe a macro for more general-purpose, shallow object comparison?
#proc `==`*(a, b: ProjectDef): bool =
template shallowEquals(a, b: RootObj): bool =
if type(a) != type(b): return false
var anyB = toAny(b)
for name, value in a.fieldPairs:
if value != b[name]: return false
return true
#proc `==`*(a, b: ProjectDef): bool = result = shallowEquals(a, b)