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)