206 lines
6.8 KiB
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)
|