Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
ce7d4b60de | |||
2622877db5 | |||
c6be698572 | |||
07037616ac | |||
b85cf8b367 | |||
741124b734 | |||
a4e6a4cb81 | |||
dcf82d8999 | |||
6556a86209 | |||
ff7f570ab1 | |||
d1f04951e5 | |||
f87dcc344b |
17
TODO.md
17
TODO.md
@ -1,6 +1,11 @@
|
|||||||
* Write a tool to convert JSON Schema into a human-readable format suitable for
|
TODO
|
||||||
documentation. Should use the description, title, and other fields from the
|
|
||||||
JSON spec. Use this for writing the JSON schema docs instead of duplicating
|
* Orchestration of docker containers for running builds.
|
||||||
the description of configuration files between JSON schema and the
|
* Write API docs.
|
||||||
documentation. In other words, use the schemas as the single source of truth
|
|
||||||
and generate everything else from that.
|
NICE TO HAVE
|
||||||
|
|
||||||
|
* Use/create some json-schema -> nim code generator to auto-generate json
|
||||||
|
handling code from schemas.
|
||||||
|
* Use some json-schema -> docs generator to document the API.
|
||||||
|
* Support unique UUID prefixes in URLs.
|
||||||
|
@ -4,7 +4,7 @@ import strawbosspkg/configuration
|
|||||||
import strawbosspkg/core
|
import strawbosspkg/core
|
||||||
import strawbosspkg/server
|
import strawbosspkg/server
|
||||||
|
|
||||||
let SB_VER = "0.2.0"
|
let SB_VER = "0.4.0"
|
||||||
|
|
||||||
proc logProcOutput*(outMsg, errMsg: TaintedString, cmd: string) =
|
proc logProcOutput*(outMsg, errMsg: TaintedString, cmd: string) =
|
||||||
let prefix = if cmd != nil: cmd & ": " else: ""
|
let prefix = if cmd != nil: cmd & ": " else: ""
|
||||||
@ -18,9 +18,10 @@ Usage:
|
|||||||
strawboss serve [options]
|
strawboss serve [options]
|
||||||
strawboss run <requestFile> [options]
|
strawboss run <requestFile> [options]
|
||||||
strawboss hashpwd <pwd>
|
strawboss hashpwd <pwd>
|
||||||
|
strawboss api-key <username>
|
||||||
|
|
||||||
Options
|
Options
|
||||||
|
|
||||||
-c --config-file <cfgFile> Use this config file instead of the default
|
-c --config-file <cfgFile> Use this config file instead of the default
|
||||||
(strawboss.config.json).
|
(strawboss.config.json).
|
||||||
"""
|
"""
|
||||||
@ -68,3 +69,6 @@ Options
|
|||||||
echo pwd
|
echo pwd
|
||||||
echo pwd[0..28]
|
echo pwd[0..28]
|
||||||
|
|
||||||
|
elif args["api-key"]:
|
||||||
|
let sessionToken = server.makeApiKey(cfg, $args["<username>"])
|
||||||
|
echo sessionToken
|
||||||
|
@ -10,7 +10,7 @@ const ISO_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:sszzz"
|
|||||||
#
|
#
|
||||||
type
|
type
|
||||||
BuildState* {.pure.} = enum
|
BuildState* {.pure.} = enum
|
||||||
queued, complete, failed, running, setup, rejected
|
complete, failed, queued, rejected, running, setup, stepComplete
|
||||||
|
|
||||||
BuildStatus* = object
|
BuildStatus* = object
|
||||||
runId*, details*: string
|
runId*, details*: string
|
||||||
@ -41,6 +41,10 @@ type
|
|||||||
request*: RunRequest
|
request*: RunRequest
|
||||||
status*: BuildStatus
|
status*: BuildStatus
|
||||||
|
|
||||||
|
RunLogs* = object
|
||||||
|
runId*: UUID
|
||||||
|
stdout*, stderr*: seq[string]
|
||||||
|
|
||||||
User* = object
|
User* = object
|
||||||
name*: string
|
name*: string
|
||||||
hashedPwd*: string
|
hashedPwd*: string
|
||||||
@ -296,8 +300,15 @@ proc `%`*(run: Run): JsonNode =
|
|||||||
"request": %run.request,
|
"request": %run.request,
|
||||||
"status": %run.status }
|
"status": %run.status }
|
||||||
|
|
||||||
|
proc `%`*(logs: RunLogs): JsonNode =
|
||||||
|
result = %* {
|
||||||
|
"runId": $logs.runId,
|
||||||
|
"stdout": %logs.stdout,
|
||||||
|
"stderr": %logs.stderr }
|
||||||
|
|
||||||
proc `$`*(s: BuildStatus): string = result = pretty(%s)
|
proc `$`*(s: BuildStatus): string = result = pretty(%s)
|
||||||
proc `$`*(req: RunRequest): string = result = pretty(%req)
|
proc `$`*(req: RunRequest): string = result = pretty(%req)
|
||||||
proc `$`*(pd: ProjectDef): string = result = pretty(%pd)
|
proc `$`*(pd: ProjectDef): string = result = pretty(%pd)
|
||||||
proc `$`*(cfg: StrawBossConfig): string = result = pretty(%cfg)
|
proc `$`*(cfg: StrawBossConfig): string = result = pretty(%cfg)
|
||||||
proc `$`*(run: Run): string = result = pretty(%run)
|
proc `$`*(run: Run): string = result = pretty(%run)
|
||||||
|
proc `$`*(logs: RunLogs): string = result = pretty(%logs)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import cliutils, logging, json, os, osproc, sequtils, streams,
|
import cliutils, logging, json, os, ospaths, osproc, sequtils, streams,
|
||||||
strtabs, strutils, tables, times, uuids
|
strtabs, strutils, tables, times, uuids
|
||||||
|
|
||||||
import ./configuration
|
import ./configuration
|
||||||
import nre except toSeq
|
import nre except toSeq
|
||||||
from posix import link
|
from posix import link, realpath
|
||||||
from algorithm import sorted
|
from algorithm import sorted
|
||||||
|
|
||||||
type
|
type
|
||||||
@ -27,7 +27,7 @@ type
|
|||||||
projectName*: string
|
projectName*: string
|
||||||
process*: Process
|
process*: Process
|
||||||
|
|
||||||
NotFoundException = object of Exception
|
NotFoundException* = object of Exception
|
||||||
|
|
||||||
proc newCopy(w: Workspace): Workspace =
|
proc newCopy(w: Workspace): Workspace =
|
||||||
var newEnv: StringTableRef = newStringTable()
|
var newEnv: StringTableRef = newStringTable()
|
||||||
@ -82,27 +82,27 @@ proc publishStatus(wksp: Workspace, state: BuildState, details: string): void =
|
|||||||
runId: $wksp.runRequest.runId, state: state, details: details)
|
runId: $wksp.runRequest.runId, state: state, details: details)
|
||||||
|
|
||||||
# Write to our run directory, and to our version status
|
# Write to our run directory, and to our version status
|
||||||
writeFile(wksp.buildDataDir & "/runs/" &
|
writeFile(wksp.buildDataDir / "runs" /
|
||||||
$wksp.runRequest.runId & ".status.json", $wksp.status)
|
$wksp.runRequest.runId & ".status.json", $wksp.status)
|
||||||
|
|
||||||
# If we have our step we can save status to the step status
|
# If we have our step we can save status to the step status
|
||||||
if not wksp.step.name.isNilOrEmpty():
|
if not wksp.step.name.isNilOrEmpty():
|
||||||
let stepStatusDir = wksp.buildDataDir & "/status/" & wksp.step.name
|
let stepStatusDir = wksp.buildDataDir / "status" / wksp.step.name
|
||||||
if not existsDir(stepStatusDir): createDir(stepStatusDir)
|
if not existsDir(stepStatusDir): createDir(stepStatusDir)
|
||||||
writeFile(stepStatusDir & "/" & wksp.version & ".json", $wksp.status)
|
writeFile(stepStatusDir / wksp.version & ".json", $wksp.status)
|
||||||
|
|
||||||
# If we were asked to build a ref that is not the version directly (like
|
# If we were asked to build a ref that is not the version directly (like
|
||||||
# "master" or something), then let's also save our status under that name.
|
# "master" or something), then let's also save our status under that name.
|
||||||
# We're probably overwriting a prior status, but that's OK.
|
# We're probably overwriting a prior status, but that's OK.
|
||||||
if wksp.runRequest.buildRef != wksp.version:
|
if wksp.runRequest.buildRef != wksp.version:
|
||||||
writeFile(stepStatusDir & "/" & wksp.runRequest.buildRef & ".json",
|
writeFile(stepStatusDir / wksp.runRequest.buildRef & ".json",
|
||||||
$wksp.status)
|
$wksp.status)
|
||||||
|
|
||||||
wksp.outputHandler.sendStatusMsg(wksp.status)
|
wksp.outputHandler.sendStatusMsg(wksp.status)
|
||||||
|
|
||||||
proc ensureProjectDirsExist(cfg: StrawBossConfig, p: ProjectDef): void =
|
proc ensureProjectDirsExist(cfg: StrawBossConfig, p: ProjectDef): void =
|
||||||
for subdir in ["configurations", "runs", "status", "artifacts"]:
|
for subdir in ["configurations", "runs", "status", "artifacts"]:
|
||||||
let fullPath = cfg.buildDataDir & "/" & p.name & "/" & subdir
|
let fullPath = cfg.buildDataDir / p.name / subdir
|
||||||
if not existsDir(fullPath):
|
if not existsDir(fullPath):
|
||||||
createDir(fullPath)
|
createDir(fullPath)
|
||||||
|
|
||||||
@ -112,9 +112,9 @@ proc getProject*(cfg: StrawBossConfig, projectName: string): ProjectDef =
|
|||||||
## Get a project definition by name from the service configuration
|
## Get a project definition by name from the service configuration
|
||||||
let candidates = cfg.projects.filterIt(it.name == projectName)
|
let candidates = cfg.projects.filterIt(it.name == projectName)
|
||||||
if candidates.len == 0:
|
if candidates.len == 0:
|
||||||
raise newException(KeyError, "no project named " & projectName)
|
raise newException(NotFoundException, "no project named " & projectName)
|
||||||
elif candidates.len > 1:
|
elif candidates.len > 1:
|
||||||
raise newException(KeyError, "multiple projects named " & projectName)
|
raise newException(NotFoundException, "multiple projects named " & projectName)
|
||||||
else: result = candidates[0]
|
else: result = candidates[0]
|
||||||
|
|
||||||
proc setProject*(cfg: var StrawBossConfig, projectName: string, newDef: ProjectDef): void =
|
proc setProject*(cfg: var StrawBossConfig, projectName: string, newDef: ProjectDef): void =
|
||||||
@ -136,23 +136,67 @@ proc listVersions*(cfg: StrawBossConfig, projectName: string): seq[string] =
|
|||||||
ensureProjectDirsExist(cfg, project)
|
ensureProjectDirsExist(cfg, project)
|
||||||
|
|
||||||
let versionFiles = filesMatching(
|
let versionFiles = filesMatching(
|
||||||
cfg.buildDataDir & "/" & project.name & "/configurations/*.json")
|
cfg.buildDataDir / project.name / "configurations/*.json")
|
||||||
|
|
||||||
result = versionFiles.map(proc(s: string): string =
|
result = versionFiles.map(proc(s: string): string =
|
||||||
let slashIdx = s.rfind('/')
|
let slashIdx = s.rfind('/')
|
||||||
result = s[(slashIdx + 1)..^6])
|
result = s[(slashIdx + 1)..^6])
|
||||||
|
|
||||||
|
proc getBuildStatus*(cfg: StrawBossConfig,
|
||||||
|
projectName, stepName, buildRef: string): BuildStatus =
|
||||||
|
|
||||||
|
let project = cfg.getProject(projectName)
|
||||||
|
|
||||||
|
let statusFile = cfg.buildDataDir / project.name / "status" /
|
||||||
|
stepName / buildRef & ".json"
|
||||||
|
|
||||||
|
if not existsFile(statusFile):
|
||||||
|
raise newException(NotFoundException,
|
||||||
|
stepName & " has never been built for " & projectName & "@" & buildRef)
|
||||||
|
|
||||||
|
result = loadBuildStatus(statusFile)
|
||||||
|
|
||||||
|
|
||||||
|
proc listArtifacts*(cfg: StrawBossConfig,
|
||||||
|
projectName, stepName, version: string): seq[string] =
|
||||||
|
## List the artifacts that have been built for a step.
|
||||||
|
|
||||||
|
let project = cfg.getProject(projectName)
|
||||||
|
|
||||||
|
ensureProjectDirsExist(cfg, project)
|
||||||
|
|
||||||
|
let buildStatus = cfg.getBuildStatus(projectName, stepName, version)
|
||||||
|
|
||||||
|
if buildStatus.state != BuildState.complete:
|
||||||
|
raise newException(NotFoundException, "step " & stepName &
|
||||||
|
" has never been successfully built for " & projectName & "@" & version)
|
||||||
|
|
||||||
|
result = filesMatching(
|
||||||
|
cfg.buildDataDir / project.name / "artifacts" / stepName / version / "*")
|
||||||
|
.mapIt(it.extractFilename)
|
||||||
|
|
||||||
|
proc getArtifactPath*(cfg: StrawBossConfig,
|
||||||
|
projectName, stepName, version, artifactName: string): string =
|
||||||
|
|
||||||
|
let artifacts = cfg.listArtifacts(projectName, stepName, version)
|
||||||
|
if not artifacts.contains(artifactName):
|
||||||
|
raise newException(NotFoundException, "no artifact named " &
|
||||||
|
artifactName & " exists for step " & stepName & " in project " &
|
||||||
|
projectName & "@" & version)
|
||||||
|
|
||||||
|
result = cfg.buildDataDir / projectName / "artifacts" / stepName / version / artifactName
|
||||||
|
|
||||||
proc existsRun*(cfg: StrawBossConfig, projectName, runId: string): bool =
|
proc existsRun*(cfg: StrawBossConfig, projectName, runId: string): bool =
|
||||||
existsFile(cfg.buildDataDir & "/" & projectName & "/runs/" & runId & ".request.json")
|
existsFile(cfg.buildDataDir / projectName / "runs" / runId & ".request.json")
|
||||||
|
|
||||||
proc getRun*(cfg: StrawBossConfig, projectName, runId: string): Run =
|
proc getRun*(cfg: StrawBossConfig, projectName, runId: string): Run =
|
||||||
let project = cfg.getProject(projectName)
|
let project = cfg.getProject(projectName)
|
||||||
let runsPath = cfg.buildDataDir & "/" & project.name & "/runs"
|
let runsPath = cfg.buildDataDir / project.name / "runs"
|
||||||
|
|
||||||
try: result = Run(
|
try: result = Run(
|
||||||
id: parseUUID(runId),
|
id: parseUUID(runId),
|
||||||
request: loadRunRequest(runsPath & "/" & runId & ".request.json"),
|
request: loadRunRequest(runsPath / runId & ".request.json"),
|
||||||
status: loadBuildStatus(runsPath & "/" & runId & ".status.json"))
|
status: loadBuildStatus(runsPath / runId & ".status.json"))
|
||||||
except: raiseEx "unable to load run information for id " & runId
|
except: raiseEx "unable to load run information for id " & runId
|
||||||
|
|
||||||
proc listRuns*(cfg: StrawBossConfig, projectName: string): seq[Run] =
|
proc listRuns*(cfg: StrawBossConfig, projectName: string): seq[Run] =
|
||||||
@ -160,29 +204,25 @@ proc listRuns*(cfg: StrawBossConfig, projectName: string): seq[Run] =
|
|||||||
let project = cfg.getProject(projectName)
|
let project = cfg.getProject(projectName)
|
||||||
ensureProjectDirsExist(cfg, project)
|
ensureProjectDirsExist(cfg, project)
|
||||||
|
|
||||||
let runsPath = cfg.buildDataDir & "/" & project.name & "/runs"
|
let runsPath = cfg.buildDataDir / project.name / "runs"
|
||||||
let reqPaths = filesMatching(runsPath & "/*.request.json")
|
let reqPaths = filesMatching(runsPath / "*.request.json")
|
||||||
|
|
||||||
result = reqPaths.map(proc(reqPath: string): Run =
|
result = reqPaths.map(proc(reqPath: string): Run =
|
||||||
let runId = reqPath[(runsPath.len + 1)..^14]
|
let runId = reqPath[(runsPath.len + 1)..^14]
|
||||||
result = Run(
|
result = Run(
|
||||||
id: parseUUID(runId),
|
id: parseUUID(runId),
|
||||||
request: loadRunRequest(reqPath),
|
request: loadRunRequest(reqPath),
|
||||||
status: loadBuildStatus(runsPath & "/" & runId & ".status.json")))
|
status: loadBuildStatus(runsPath / runId & ".status.json")))
|
||||||
|
|
||||||
proc getBuildStatus*(cfg: StrawBossConfig,
|
|
||||||
projectName, stepName, buildRef: string): BuildStatus =
|
|
||||||
|
|
||||||
|
proc getLogs*(cfg: StrawBossConfig, projectname, runId: string): RunLogs =
|
||||||
let project = cfg.getProject(projectName)
|
let project = cfg.getProject(projectName)
|
||||||
|
let runsPath = cfg.buildDataDir / project.name / "runs"
|
||||||
|
|
||||||
let statusFile = cfg.buildDataDir & "/" & project.name & "/status/" &
|
try: result = RunLogs(
|
||||||
stepName & "/" & buildRef & ".json"
|
runId: parseUUID(runId),
|
||||||
|
stdout: toSeq(lines(runsPath / runId & ".stdout.log")),
|
||||||
if not existsFile(statusFile):
|
stderr: toSeq(lines(runsPath / runId & ".stderr.log")))
|
||||||
raise newException(NotFoundException,
|
except: raiseEx "unable to load logs for run " & runId
|
||||||
stepName & " has never been built for reference '" & buildRef)
|
|
||||||
|
|
||||||
result = loadBuildStatus(statusFile)
|
|
||||||
|
|
||||||
proc getProjectConfig*(cfg: StrawBossConfig,
|
proc getProjectConfig*(cfg: StrawBossConfig,
|
||||||
projectName, version: string): ProjectConfig =
|
projectName, version: string): ProjectConfig =
|
||||||
@ -196,7 +236,7 @@ proc getProjectConfig*(cfg: StrawBossConfig,
|
|||||||
if version.isNilOrEmpty:
|
if version.isNilOrEmpty:
|
||||||
|
|
||||||
let candidatePaths = filesMatching(
|
let candidatePaths = filesMatching(
|
||||||
cfg.buildDataDir & "/" & project.name & "/configurations/*.json")
|
cfg.buildDataDir / project.name / "configurations/*.json")
|
||||||
|
|
||||||
if candidatePaths.len == 0:
|
if candidatePaths.len == 0:
|
||||||
raise newException(NotFoundException,
|
raise newException(NotFoundException,
|
||||||
@ -212,8 +252,7 @@ proc getProjectConfig*(cfg: StrawBossConfig,
|
|||||||
# If they did, let's try to load that
|
# If they did, let's try to load that
|
||||||
else:
|
else:
|
||||||
confFilePath =
|
confFilePath =
|
||||||
cfg.buildDataDir & "/" & project.name & "/configurations/" &
|
cfg.buildDataDir / project.name / "configurations" / version & ".json"
|
||||||
version & ".json"
|
|
||||||
|
|
||||||
if not existsFile(confFilePath):
|
if not existsFile(confFilePath):
|
||||||
raise newException(NotFoundException,
|
raise newException(NotFoundException,
|
||||||
@ -228,7 +267,7 @@ proc setupProject(wksp: Workspace) =
|
|||||||
wksp.sendMsg(lvlDebug, "Setting up project.")
|
wksp.sendMsg(lvlDebug, "Setting up project.")
|
||||||
|
|
||||||
# Clone the project into the $temp directory
|
# Clone the project into the $temp directory
|
||||||
let cloneArgs = ["clone", wksp.projectDef.repo, wksp.dir]
|
let cloneArgs = @["clone", wksp.projectDef.repo, wksp.dir]
|
||||||
wksp.sendMsg(lvlDebug, "git " & $cloneArgs)
|
wksp.sendMsg(lvlDebug, "git " & $cloneArgs)
|
||||||
|
|
||||||
let cloneResult = exec("git", ".", cloneArgs, wksp.env, {poUsePath},
|
let cloneResult = exec("git", ".", cloneArgs, wksp.env, {poUsePath},
|
||||||
@ -238,7 +277,7 @@ proc setupProject(wksp: Workspace) =
|
|||||||
raiseEx "unable to clone repo for '" & wksp.projectDef.name & "'"
|
raiseEx "unable to clone repo for '" & wksp.projectDef.name & "'"
|
||||||
|
|
||||||
# Checkout the requested ref
|
# Checkout the requested ref
|
||||||
let checkoutArgs = ["checkout", wksp.buildRef]
|
let checkoutArgs = @["checkout", wksp.buildRef]
|
||||||
wksp.sendMsg(lvlDebug, "git " & $checkoutArgs)
|
wksp.sendMsg(lvlDebug, "git " & $checkoutArgs)
|
||||||
|
|
||||||
let checkoutResult = exec("git", wksp.dir, checkoutArgs,
|
let checkoutResult = exec("git", wksp.dir, checkoutArgs,
|
||||||
@ -249,7 +288,7 @@ proc setupProject(wksp: Workspace) =
|
|||||||
" for '" & wksp.projectDef.name & "'"
|
" for '" & wksp.projectDef.name & "'"
|
||||||
|
|
||||||
# Find the strawboss project configuration
|
# Find the strawboss project configuration
|
||||||
let projCfgFile = wksp.dir & "/" & wksp.projectDef.cfgFilePath
|
let projCfgFile = wksp.dir / wksp.projectDef.cfgFilePath
|
||||||
wksp.sendMsg(lvlDebug, "Looking for project configuration at '" & projCfgFile & "'")
|
wksp.sendMsg(lvlDebug, "Looking for project configuration at '" & projCfgFile & "'")
|
||||||
if not existsFile(projCfgFile):
|
if not existsFile(projCfgFile):
|
||||||
raiseEx "Cannot find strawboss project configuration in the project " &
|
raiseEx "Cannot find strawboss project configuration in the project " &
|
||||||
@ -283,21 +322,20 @@ proc doStep*(wksp: Workspace, step: Step): BuildStatus =
|
|||||||
|
|
||||||
wksp.step = step
|
wksp.step = step
|
||||||
|
|
||||||
let artifactsDir = wksp.buildDataDir & "/artifacts/" &
|
let artifactsDir = wksp.buildDataDir / "artifacts" / step.name / wksp.version
|
||||||
step.name & "/" & wksp.version
|
|
||||||
|
|
||||||
if not existsDir(artifactsDir): createDir(artifactsDir)
|
if not existsDir(artifactsDir): createDir(artifactsDir)
|
||||||
|
|
||||||
# Have we tried to build this before and are we caching the results?
|
# Have we tried to build this before and are we caching the results?
|
||||||
let statusFilePath = wksp.buildDataDir & "/status/" & step.name &
|
let statusFilePath = wksp.buildDataDir / "status" / step.name /
|
||||||
"/" & wksp.version & ".json"
|
wksp.version & ".json"
|
||||||
|
|
||||||
if existsFile(statusFilePath) and not step.dontSkip:
|
if existsFile(statusFilePath) and not step.dontSkip:
|
||||||
let prevStatus = loadBuildStatus(statusFilePath)
|
let prevStatus = loadBuildStatus(statusFilePath)
|
||||||
|
|
||||||
# If we succeeded last time, no need to rebuild
|
# If we succeeded last time, no need to rebuild
|
||||||
if prevStatus.state == BuildState.complete:
|
if prevStatus.state == BuildState.complete:
|
||||||
wksp.publishStatus(BuildState.complete,
|
wksp.publishStatus(BuildState.stepComplete,
|
||||||
"Skipping step '" & step.name & "' for version '" & wksp.version &
|
"Skipping step '" & step.name & "' for version '" & wksp.version &
|
||||||
"': already completed.")
|
"': already completed.")
|
||||||
return wksp.status
|
return wksp.status
|
||||||
@ -328,7 +366,7 @@ proc doStep*(wksp: Workspace, step: Step): BuildStatus =
|
|||||||
# Run that step (may get skipped)
|
# Run that step (may get skipped)
|
||||||
let runStatus = doStep(core.newCopy(wksp), depStep)
|
let runStatus = doStep(core.newCopy(wksp), depStep)
|
||||||
|
|
||||||
if not (runStatus.state == BuildState.complete):
|
if not (runStatus.state == BuildState.stepComplete):
|
||||||
raiseEx "dependent step failed: " & depStep.name
|
raiseEx "dependent step failed: " & depStep.name
|
||||||
|
|
||||||
wksp.sendMsg(lvlDebug, "dependent step '" & depStep.name &
|
wksp.sendMsg(lvlDebug, "dependent step '" & depStep.name &
|
||||||
@ -336,8 +374,8 @@ proc doStep*(wksp: Workspace, step: Step): BuildStatus =
|
|||||||
|
|
||||||
# Add the artifacts directory for the dependent step to our env so that
|
# Add the artifacts directory for the dependent step to our env so that
|
||||||
# further steps can reference it via $<stepname>_DIR
|
# further steps can reference it via $<stepname>_DIR
|
||||||
wksp.env[depStep.name & "_DIR"] = wksp.buildDataDir & "/artifacts/" &
|
wksp.env[depStep.name & "_DIR"] = wksp.buildDataDir / "artifacts" /
|
||||||
dep & "/" & wksp.version
|
dep / wksp.version
|
||||||
|
|
||||||
# Run the step command, piping in cmdInput
|
# Run the step command, piping in cmdInput
|
||||||
let stepCmd = wksp.resolveEnvVars(step.stepCmd)
|
let stepCmd = wksp.resolveEnvVars(step.stepCmd)
|
||||||
@ -345,7 +383,7 @@ proc doStep*(wksp: Workspace, step: Step): BuildStatus =
|
|||||||
else: stepCmd
|
else: stepCmd
|
||||||
wksp.sendMsg step.name & ": starting stepCmd: " & stepCmd
|
wksp.sendMsg step.name & ": starting stepCmd: " & stepCmd
|
||||||
let cmdProc = startProcess(stepCmd,
|
let cmdProc = startProcess(stepCmd,
|
||||||
wksp.dir & "/" & step.workingDir, [], wksp.env, {poUsePath, poEvalCommand})
|
wksp.dir / step.workingDir, [], wksp.env, {poUsePath, poEvalCommand})
|
||||||
|
|
||||||
let cmdInStream = inputStream(cmdProc)
|
let cmdInStream = inputStream(cmdProc)
|
||||||
|
|
||||||
@ -353,7 +391,7 @@ proc doStep*(wksp: Workspace, step: Step): BuildStatus =
|
|||||||
for line in step.cmdInput: cmdInStream.writeLine(wksp.resolveEnvVars(line))
|
for line in step.cmdInput: cmdInStream.writeLine(wksp.resolveEnvVars(line))
|
||||||
cmdInStream.flush()
|
cmdInStream.flush()
|
||||||
cmdInStream.close()
|
cmdInStream.close()
|
||||||
|
|
||||||
let cmdResult = waitFor(cmdProc, wksp.outputHandler, cmdName)
|
let cmdResult = waitFor(cmdProc, wksp.outputHandler, cmdName)
|
||||||
|
|
||||||
if cmdResult != 0:
|
if cmdResult != 0:
|
||||||
@ -367,16 +405,16 @@ proc doStep*(wksp: Workspace, step: Step): BuildStatus =
|
|||||||
let artifactName = artifactPath[(artifactPath.rfind("/")+1)..^1]
|
let artifactName = artifactPath[(artifactPath.rfind("/")+1)..^1]
|
||||||
try:
|
try:
|
||||||
wksp.sendMsg "copy " &
|
wksp.sendMsg "copy " &
|
||||||
wksp.dir & "/" & step.workingDir & "/" & artifactPath & " -> " &
|
wksp.dir / step.workingDir / artifactPath & " -> " &
|
||||||
artifactsDir & "/" & artifactName
|
artifactsDir / artifactName
|
||||||
|
|
||||||
copyFileWithPermissions(wksp.dir & "/" & step.workingDir & "/" &
|
copyFileWithPermissions(wksp.dir / step.workingDir / artifactPath,
|
||||||
artifactPath, artifactsDir & "/" & artifactName)
|
artifactsDir / artifactName)
|
||||||
except:
|
except:
|
||||||
raiseEx "step " & step.name & " failed: unable to copy artifact " &
|
raiseEx "step " & step.name & " failed: unable to copy artifact " &
|
||||||
artifactPath & ":\n" & getCurrentExceptionMsg()
|
artifactPath & ":\n" & getCurrentExceptionMsg()
|
||||||
|
|
||||||
wksp.publishStatus(BuildState.complete, "")
|
wksp.publishStatus(BuildState.stepComplete, "step " & step.name & " complete")
|
||||||
result = wksp.status
|
result = wksp.status
|
||||||
|
|
||||||
proc run*(cfg: StrawBossConfig, req: RunRequest,
|
proc run*(cfg: StrawBossConfig, req: RunRequest,
|
||||||
@ -401,8 +439,8 @@ proc run*(cfg: StrawBossConfig, req: RunRequest,
|
|||||||
ensureProjectDirsExist(cfg, projectDef)
|
ensureProjectDirsExist(cfg, projectDef)
|
||||||
|
|
||||||
# Update our run status
|
# Update our run status
|
||||||
let runDir = cfg.buildDataDir & "/" & projectDef.name & "/runs"
|
let runDir = cfg.buildDataDir / projectDef.name / "runs"
|
||||||
writeFile(runDir & "/" & $req.runId & ".status.json", $result)
|
writeFile(runDir / $req.runId & ".status.json", $result)
|
||||||
|
|
||||||
# Read in the existing system environment
|
# Read in the existing system environment
|
||||||
var env = loadEnv()
|
var env = loadEnv()
|
||||||
@ -413,13 +451,13 @@ proc run*(cfg: StrawBossConfig, req: RunRequest,
|
|||||||
if not existsDir(req.workspaceDir): createDir(req.workspaceDir)
|
if not existsDir(req.workspaceDir): createDir(req.workspaceDir)
|
||||||
|
|
||||||
# Setup our STDOUT and STDERR files
|
# Setup our STDOUT and STDERR files
|
||||||
let stdoutFile = open(runDir & "/" & $req.runId & ".stdout.log", fmWrite)
|
let stdoutFile = open(runDir / $req.runId & ".stdout.log", fmWrite)
|
||||||
let stderrFile = open(runDir & "/" & $req.runId & ".stderr.log", fmWrite)
|
let stderrFile = open(runDir / $req.runId & ".stderr.log", fmWrite)
|
||||||
|
|
||||||
let logFilesOH = makeProcMsgHandler(stdoutFile, stderrFile)
|
let logFilesOH = makeProcMsgHandler(stdoutFile, stderrFile)
|
||||||
|
|
||||||
wksp = Workspace(
|
wksp = Workspace(
|
||||||
buildDataDir: cfg.buildDataDir & "/" & projectDef.name,
|
buildDataDir: cfg.buildDataDir / projectDef.name,
|
||||||
buildRef:
|
buildRef:
|
||||||
if req.buildRef != nil and req.buildRef.len > 0: req.buildRef
|
if req.buildRef != nil and req.buildRef.len > 0: req.buildRef
|
||||||
else: projectDef.defaultBranch,
|
else: projectDef.defaultBranch,
|
||||||
@ -452,8 +490,8 @@ proc run*(cfg: StrawBossConfig, req: RunRequest,
|
|||||||
# Update our cache of project configurations.
|
# Update our cache of project configurations.
|
||||||
# TODO: what happens if this fails?
|
# TODO: what happens if this fails?
|
||||||
copyFileWithPermissions(
|
copyFileWithPermissions(
|
||||||
wksp.dir & "/" & wksp.projectDef.cfgFilePath,
|
wksp.dir / wksp.projectDef.cfgFilePath,
|
||||||
wksp.buildDataDir & "/configurations/" & wksp.version & ".json")
|
wksp.buildDataDir / "configurations" / wksp.version & ".json")
|
||||||
|
|
||||||
# Find the requested step
|
# Find the requested step
|
||||||
if not wksp.project.steps.hasKey(req.stepName):
|
if not wksp.project.steps.hasKey(req.stepName):
|
||||||
@ -462,7 +500,12 @@ proc run*(cfg: StrawBossConfig, req: RunRequest,
|
|||||||
|
|
||||||
if req.forceRebuild: step.dontSkip = true
|
if req.forceRebuild: step.dontSkip = true
|
||||||
|
|
||||||
result = doStep(wksp, step)
|
var buildStatus = doStep(wksp, step)
|
||||||
|
if buildStatus.state == BuildState.stepComplete:
|
||||||
|
buildStatus.state = BuildState.complete
|
||||||
|
wksp.publishStatus(buildStatus.state, "all steps complete")
|
||||||
|
|
||||||
|
result = wksp.status
|
||||||
|
|
||||||
except:
|
except:
|
||||||
when not defined(release): echo getCurrentException().getStackTrace()
|
when not defined(release): echo getCurrentException().getStackTrace()
|
||||||
@ -477,19 +520,19 @@ proc run*(cfg: StrawBossConfig, req: RunRequest,
|
|||||||
|
|
||||||
finally:
|
finally:
|
||||||
if wksp != nil:
|
if wksp != nil:
|
||||||
# Close open files
|
# Close open files
|
||||||
for f in wksp.openedFiles:
|
for f in wksp.openedFiles:
|
||||||
try: close(f)
|
try: close(f)
|
||||||
except: discard ""
|
except: discard ""
|
||||||
|
|
||||||
proc spawnWorker*(cfg: StrawBossConfig, req: RunRequest):
|
proc spawnWorker*(cfg: StrawBossConfig, req: RunRequest):
|
||||||
tuple[status: BuildStatus, worker: Worker] =
|
tuple[status: BuildStatus, worker: Worker] =
|
||||||
|
|
||||||
# Find the project definition (will throw appropriate exceptions)
|
# Find the project definition (will throw appropriate exceptions)
|
||||||
let projectDef = cfg.getProject(req.projectName)
|
let projectDef = cfg.getProject(req.projectName)
|
||||||
let runDir = cfg.buildDataDir & "/" & projectDef.name & "/runs"
|
let runDir = cfg.buildDataDir / projectDef.name / "runs"
|
||||||
let reqFile = runDir & "/" & $req.runId & ".request.json"
|
let reqFile = runDir / $req.runId & ".request.json"
|
||||||
let statusFile = runDir & "/" & $req.runId & ".status.json"
|
let statusFile = runDir / $req.runId & ".status.json"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Make sure the build data directories for this project exist.
|
# Make sure the build data directories for this project exist.
|
||||||
@ -517,8 +560,8 @@ proc spawnWorker*(cfg: StrawBossConfig, req: RunRequest):
|
|||||||
|
|
||||||
except:
|
except:
|
||||||
let exMsg = "run request rejected: " & getCurrentExceptionMsg()
|
let exMsg = "run request rejected: " & getCurrentExceptionMsg()
|
||||||
raiseEx exMsg
|
|
||||||
try:
|
try:
|
||||||
writeFile(statusFile,
|
writeFile(statusFile,
|
||||||
$(BuildStatus(runId: $req.runId, state: BuildState.rejected, details: exMsg)))
|
$(BuildStatus(runId: $req.runId, state: BuildState.rejected, details: exMsg)))
|
||||||
except: discard ""
|
except: discard ""
|
||||||
|
raiseEx exMsg
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import asyncdispatch, bcrypt, cliutils, jester, json, jwt, logging,
|
import asyncdispatch, bcrypt, cliutils, jester, json, jwt, logging, md5,
|
||||||
os, osproc, sequtils, strutils, tempfile, times, unittest, uuids
|
os, osproc, sequtils, strutils, tempfile, times, unittest, uuids
|
||||||
|
|
||||||
|
from mimetypes import getMimeType
|
||||||
|
from asyncfile import openAsync, readToStream, close
|
||||||
|
from asyncnet import send
|
||||||
|
from re import re, find
|
||||||
|
|
||||||
import ./configuration, ./core
|
import ./configuration, ./core
|
||||||
|
|
||||||
type
|
type
|
||||||
@ -11,19 +16,28 @@ type
|
|||||||
#const ISO_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
|
#const ISO_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
|
||||||
const JSON = "application/json"
|
const JSON = "application/json"
|
||||||
|
|
||||||
proc makeJsonResp(status: HttpCode, details: string = ""): string =
|
|
||||||
result = $(%* {
|
|
||||||
"statusCode": status.int,
|
|
||||||
"status": $status,
|
|
||||||
"details": details
|
|
||||||
})
|
|
||||||
|
|
||||||
proc newSession*(user: UserRef): Session =
|
proc newSession*(user: UserRef): Session =
|
||||||
result = Session(
|
result = Session(
|
||||||
user: user,
|
user: user,
|
||||||
issuedAt: getTime(),
|
issuedAt: getTime(),
|
||||||
expires: daysForward(7).toTime())
|
expires: daysForward(7).toTime())
|
||||||
|
|
||||||
|
proc buildJson(resp: Response, code: HttpCode, details: string = ""): void =
|
||||||
|
resp.data[0] = CallbackAction.TCActionSend
|
||||||
|
resp.data[1] = code
|
||||||
|
resp.data[2]["Content-Type"] = JSON
|
||||||
|
resp.data[3] = $(%* {
|
||||||
|
"statusCode": code.int,
|
||||||
|
"status": $code,
|
||||||
|
"details": details
|
||||||
|
})
|
||||||
|
|
||||||
|
# Work-around for weirdness trying to use resp(Http500... in exception blocks
|
||||||
|
proc build500Json(resp: Response, ex: ref Exception, msg: string): void =
|
||||||
|
when not defined(release): debug ex.getStackTrace()
|
||||||
|
error msg & ":\n" & ex.msg
|
||||||
|
resp.buildJson(Http500)
|
||||||
|
|
||||||
proc toJWT*(cfg: StrawBossConfig, session: Session): string =
|
proc toJWT*(cfg: StrawBossConfig, session: Session): string =
|
||||||
## Make a JST token for this session.
|
## Make a JST token for this session.
|
||||||
var jwt = JWT(
|
var jwt = JWT(
|
||||||
@ -89,7 +103,30 @@ proc makeAuthToken*(cfg: StrawBossConfig, uname, pwd: string): string =
|
|||||||
let user = users[0]
|
let user = users[0]
|
||||||
|
|
||||||
if not validatePwd(user, pwd): raiseEx "invalid username or password"
|
if not validatePwd(user, pwd): raiseEx "invalid username or password"
|
||||||
result = toJWT(cfg, newSession(user))
|
|
||||||
|
let session = newSession(user)
|
||||||
|
|
||||||
|
result = toJWT(cfg, session)
|
||||||
|
|
||||||
|
proc makeApiKey*(cfg: StrawBossConfig, uname: string): string =
|
||||||
|
## Given a username, make an API token (JWT token string that does not
|
||||||
|
## expire). Note that this does not validate the username/pwd combination. It
|
||||||
|
## is not intended to be exposed publicly via the API, but serve as a utility
|
||||||
|
## function for an administrator to setup a unsupervised account (git access
|
||||||
|
## for example).
|
||||||
|
|
||||||
|
if uname == nil: raiseEx "no username given"
|
||||||
|
|
||||||
|
# find the user record
|
||||||
|
let users = cfg.users.filterIt(it.name == uname)
|
||||||
|
if users.len != 1: raiseEx "invalid username"
|
||||||
|
|
||||||
|
let session = Session(
|
||||||
|
user: users[0],
|
||||||
|
issuedAt: getTime(),
|
||||||
|
expires: daysForward(365 * 1000).toTime())
|
||||||
|
|
||||||
|
result = toJWT(cfg, session);
|
||||||
|
|
||||||
template checkAuth() =
|
template checkAuth() =
|
||||||
## Check this request for authentication and authorization information.
|
## Check this request for authentication and authorization information.
|
||||||
@ -106,7 +143,8 @@ template checkAuth() =
|
|||||||
authed = true
|
authed = true
|
||||||
except:
|
except:
|
||||||
debug "Auth failed: " & getCurrentExceptionMsg()
|
debug "Auth failed: " & getCurrentExceptionMsg()
|
||||||
resp(Http401, makeJsonResp(Http401), JSON)
|
response.data[2]["WWW-Authenticate"] = "Bearer"
|
||||||
|
response.buildJson(Http401)
|
||||||
|
|
||||||
proc start*(cfg: StrawBossConfig): void =
|
proc start*(cfg: StrawBossConfig): void =
|
||||||
|
|
||||||
@ -119,18 +157,21 @@ proc start*(cfg: StrawBossConfig): void =
|
|||||||
|
|
||||||
routes:
|
routes:
|
||||||
|
|
||||||
|
get "/ping":
|
||||||
|
resp($(%"pong"), JSON)
|
||||||
|
|
||||||
post "/auth-token":
|
post "/auth-token":
|
||||||
var uname, pwd: string
|
var uname, pwd: string
|
||||||
try:
|
try:
|
||||||
let jsonBody = parseJson(request.body)
|
let jsonBody = parseJson(request.body)
|
||||||
uname = jsonBody["username"].getStr
|
uname = jsonBody["username"].getStr
|
||||||
pwd = jsonBody["password"].getStr
|
pwd = jsonBody["password"].getStr
|
||||||
except: resp(Http400, makeJsonResp(Http400), JSON)
|
except: response.buildJson(Http400); return true
|
||||||
|
|
||||||
try:
|
try:
|
||||||
let authToken = makeAuthToken(cfg, uname, pwd)
|
let authToken = makeAuthToken(cfg, uname, pwd)
|
||||||
resp($(%authToken), JSON)
|
resp($(%authToken), JSON)
|
||||||
except: resp(Http401, makeJsonResp(Http401, getCurrentExceptionMsg()), JSON)
|
except: response.buildJson(Http401, getCurrentExceptionMsg()); return true
|
||||||
|
|
||||||
get "/verify-auth":
|
get "/verify-auth":
|
||||||
checkAuth(); if not authed: return true
|
checkAuth(); if not authed: return true
|
||||||
@ -150,7 +191,7 @@ proc start*(cfg: StrawBossConfig): void =
|
|||||||
checkAuth(); if not authed: return true
|
checkAuth(); if not authed: return true
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
resp(Http501, makeJsonResp(Http501), JSON)
|
response.buildJson(Http501); return true
|
||||||
|
|
||||||
get "/project/@projectName":
|
get "/project/@projectName":
|
||||||
## Return a project's configuration, as well as it's versions.
|
## Return a project's configuration, as well as it's versions.
|
||||||
@ -160,7 +201,14 @@ proc start*(cfg: StrawBossConfig): void =
|
|||||||
# Make sure we know about that project
|
# Make sure we know about that project
|
||||||
var projDef: ProjectDef
|
var projDef: ProjectDef
|
||||||
try: projDef = cfg.getProject(@"projectName")
|
try: projDef = cfg.getProject(@"projectName")
|
||||||
except: resp(Http404, makeJsonResp(Http404, getCurrentExceptionMsg()), JSON)
|
except:
|
||||||
|
try: raise getCurrentException()
|
||||||
|
except NotFoundException:
|
||||||
|
response.buildJson(Http404, getCurrentExceptionMsg())
|
||||||
|
except:
|
||||||
|
response.build500Json(getCurrentException(),
|
||||||
|
"unable to load project definition for project " & @"projectName")
|
||||||
|
return true
|
||||||
|
|
||||||
var projConf: ProjectConfig
|
var projConf: ProjectConfig
|
||||||
try: projConf = getProjectConfig(cfg, @"projectName", "")
|
try: projConf = getProjectConfig(cfg, @"projectName", "")
|
||||||
@ -181,13 +229,13 @@ proc start*(cfg: StrawBossConfig): void =
|
|||||||
|
|
||||||
try: resp($(%listVersions(cfg, @"projectName")), JSON)
|
try: resp($(%listVersions(cfg, @"projectName")), JSON)
|
||||||
except:
|
except:
|
||||||
if getCurrentException() is KeyError:
|
try: raise getCurrentException()
|
||||||
resp(Http404, makeJsonResp(Http404, getCurrentExceptionMsg()), JSON)
|
except NotFoundException:
|
||||||
else:
|
response.buildJson(Http404, getCurrentExceptionMsg())
|
||||||
when not defined(release): debug getCurrentException().getStackTrace()
|
except:
|
||||||
error "unable to list versions for project " & @"projectName" &
|
response.build500Json(getCurrentException(),
|
||||||
":\n" & getCurrentExceptionMsg()
|
"unable to list versions for project " & @"projectName")
|
||||||
resp(Http500, makeJsonResp(Http500, "internal server error"), JSON)
|
return true
|
||||||
|
|
||||||
get "/project/@projectName/version/@version?":
|
get "/project/@projectName/version/@version?":
|
||||||
## Get a detailed project record including step definitions (ProjectConfig).
|
## Get a detailed project record including step definitions (ProjectConfig).
|
||||||
@ -196,7 +244,7 @@ proc start*(cfg: StrawBossConfig): void =
|
|||||||
|
|
||||||
# Make sure we know about that project
|
# Make sure we know about that project
|
||||||
try: resp($(%getProjectConfig(cfg, @"projectName", @"version")), JSON)
|
try: resp($(%getProjectConfig(cfg, @"projectName", @"version")), JSON)
|
||||||
except: resp(Http404, makeJsonResp(Http404, getCurrentExceptionMsg()), JSON)
|
except: response.buildJson(Http404, getCurrentExceptionMsg()); return true
|
||||||
|
|
||||||
get "/project/@projectName/runs":
|
get "/project/@projectName/runs":
|
||||||
## List all runs
|
## List all runs
|
||||||
@ -204,7 +252,7 @@ proc start*(cfg: StrawBossConfig): void =
|
|||||||
checkAuth(); if not authed: return true
|
checkAuth(); if not authed: return true
|
||||||
|
|
||||||
try: resp($(%listRuns(cfg, @"projectName")), JSON)
|
try: resp($(%listRuns(cfg, @"projectName")), JSON)
|
||||||
except: resp(Http404, makeJsonResp(Http404, getCurrentExceptionMsg()), JSON)
|
except: response.buildJson(Http404, getCurrentExceptionMsg()); return true
|
||||||
|
|
||||||
get "/project/@projectName/runs/active":
|
get "/project/@projectName/runs/active":
|
||||||
## List all currently active runs
|
## List all currently active runs
|
||||||
@ -217,12 +265,13 @@ proc start*(cfg: StrawBossConfig): void =
|
|||||||
.mapIt(cfg.getRun(@"projecName", $it.runId));
|
.mapIt(cfg.getRun(@"projecName", $it.runId));
|
||||||
resp($(%activeRuns), JSON)
|
resp($(%activeRuns), JSON)
|
||||||
except:
|
except:
|
||||||
if getCurrentException() is KeyError:
|
try: raise getCurrentException()
|
||||||
resp(Http404, makeJsonResp(Http404, getCurrentExceptionMsg()), JSON)
|
except NotFoundException:
|
||||||
else:
|
response.buildJson(Http404, getCurrentExceptionMsg())
|
||||||
when not defined(release): debug getCurrentException().getStackTrace()
|
except:
|
||||||
error "problem loading active runs: " & getCurrentExceptionMsg()
|
response.build500Json(getCurrentException(),
|
||||||
resp(Http500, makeJsonResp(Http500, "internal server error"), JSON)
|
"problem loading active runs")
|
||||||
|
return true
|
||||||
|
|
||||||
get "/project/@projectName/run/@runId":
|
get "/project/@projectName/run/@runId":
|
||||||
## Details for a specific run
|
## Details for a specific run
|
||||||
@ -231,13 +280,115 @@ proc start*(cfg: StrawBossConfig): void =
|
|||||||
|
|
||||||
# Make sure we know about that project
|
# Make sure we know about that project
|
||||||
try: discard cfg.getProject(@"projectName")
|
try: discard cfg.getProject(@"projectName")
|
||||||
except: resp(Http404, makeJsonResp(Http404, getCurrentExceptionMsg()), JSON)
|
except: response.buildJson(Http404, getCurrentExceptionMsg()); return true
|
||||||
|
|
||||||
if not existsRun(cfg, @"projectName", @"runId"):
|
if not existsRun(cfg, @"projectName", @"runId"):
|
||||||
resp(Http404, makeJsonResp(Http404, "no such run for project"), JSON)
|
response.buildJson(Http404, "no such run for project"); return true
|
||||||
|
|
||||||
try: resp($getRun(cfg, @"projectName", @"runId"), JSON)
|
try: resp($getRun(cfg, @"projectName", @"runId"), JSON)
|
||||||
except: resp(Http500, makeJsonResp(Http500, getCurrentExceptionMsg()), JSON)
|
except:
|
||||||
|
response.build500Json(getCurrentException(),
|
||||||
|
"unable to load run details for project " & @"projectName" &
|
||||||
|
" run " & @"runId")
|
||||||
|
return true
|
||||||
|
|
||||||
|
get "/project/@projectName/run/@runId/logs":
|
||||||
|
## Get logs from a specific run
|
||||||
|
|
||||||
|
checkAuth(); if not authed: return true
|
||||||
|
|
||||||
|
try: discard cfg.getProject(@"projectName")
|
||||||
|
except:
|
||||||
|
response.buildJson(Http404, getCurrentExceptionMsg())
|
||||||
|
return true
|
||||||
|
|
||||||
|
if not existsRun(cfg, @"projectName", @"runId"):
|
||||||
|
response.buildJson(Http404, "no such run for project")
|
||||||
|
return true
|
||||||
|
|
||||||
|
try: resp($getLogs(cfg, @"projectName", @"runId"))
|
||||||
|
except:
|
||||||
|
response.build500Json(getCurrentException(),
|
||||||
|
"unable to load run logs for " & @"projectName" & " run " & @"runId")
|
||||||
|
return true
|
||||||
|
|
||||||
|
get "/project/@projectName/step/@stepName/artifacts/@version":
|
||||||
|
## Get the list of artifacts that were built for
|
||||||
|
|
||||||
|
checkAuth(); if not authed: return true
|
||||||
|
|
||||||
|
debug "Matched artifacts list request: " & $(%*{
|
||||||
|
"project": @"projectName",
|
||||||
|
"step": @"stepName",
|
||||||
|
"version": @"version"
|
||||||
|
})
|
||||||
|
|
||||||
|
try: resp($(%listArtifacts(cfg, @"projectName", @"stepName", @"version")), JSON)
|
||||||
|
except:
|
||||||
|
try: raise getCurrentException()
|
||||||
|
except NotFoundException:
|
||||||
|
response.buildJson(Http404, getCurrentExceptionMsg())
|
||||||
|
except:
|
||||||
|
response.build500Json(getCurrentException(), "unable to list artifacts for " &
|
||||||
|
@"projectName" & ":" & @"stepName" & "@" & @"buildRef")
|
||||||
|
return true
|
||||||
|
|
||||||
|
get "/project/@projectName/step/@stepName/artifact/@version/@artifactName":
|
||||||
|
## Get a specific artifact that was built.
|
||||||
|
|
||||||
|
checkAuth(); if not authed: return true
|
||||||
|
|
||||||
|
var artifactPath: string
|
||||||
|
try: artifactPath = getArtifactPath(cfg,
|
||||||
|
@"projectName", @"stepName", @"version", @"artifactName")
|
||||||
|
except:
|
||||||
|
try: raise getCurrentException()
|
||||||
|
except NotFoundException:
|
||||||
|
response.buildJson(Http404, getCurrentExceptionMsg())
|
||||||
|
except:
|
||||||
|
response.build500Json(getCurrentException(), "unable to check artifact path for " &
|
||||||
|
@"projectName" & ":" & @"stepName" & "@" & @"version")
|
||||||
|
return true
|
||||||
|
|
||||||
|
debug "Preparing: " & artifactPath
|
||||||
|
let fileSize = getFileSize(artifactPath)
|
||||||
|
let mimetype = request.settings.mimes.getMimetype(artifactPath.splitFile.ext[1 .. ^1])
|
||||||
|
if fileSize < 10_000_000: # 10 mb
|
||||||
|
var file = readFile(artifactPath)
|
||||||
|
|
||||||
|
var hashed = getMD5(file)
|
||||||
|
|
||||||
|
# If the user has a cached version of this file and it matches our
|
||||||
|
# version, let them use it
|
||||||
|
if request.headers.hasKey("If-None-Match") and request.headers["If-None-Match"] == hashed:
|
||||||
|
resp(Http304, [], "")
|
||||||
|
else:
|
||||||
|
resp(Http200, [
|
||||||
|
("Content-Disposition", "; filename=\"" & @"artifactName" & "\""),
|
||||||
|
("Content-Type", mimetype),
|
||||||
|
("ETag", hashed )], file)
|
||||||
|
else:
|
||||||
|
let headers = {
|
||||||
|
"Content-Disposition": "; filename=\"" & @"artifactName" & "\"",
|
||||||
|
"Content-Type": mimetype,
|
||||||
|
"Content-Length": $fileSize
|
||||||
|
}.newStringTable
|
||||||
|
await response.sendHeaders(Http200, headers)
|
||||||
|
|
||||||
|
var fileStream = newFutureStream[string]("sendStaticIfExists")
|
||||||
|
var file = openAsync(artifactPath, fmRead)
|
||||||
|
# Let `readToStream` write file data into fileStream in the
|
||||||
|
# background.
|
||||||
|
asyncCheck file.readToStream(fileStream)
|
||||||
|
# The `writeFromStream` proc will complete once all the data in the
|
||||||
|
# `bodyStream` has been written to the file.
|
||||||
|
while true:
|
||||||
|
let (hasValue, value) = await fileStream.read()
|
||||||
|
if hasValue:
|
||||||
|
await response.client.send(value)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
file.close()
|
||||||
|
|
||||||
get "/project/@projectName/step/@stepName/status/@buildRef":
|
get "/project/@projectName/step/@stepName/status/@buildRef":
|
||||||
## Get detailed information about the status of a step (assuming it has been built)
|
## Get detailed information about the status of a step (assuming it has been built)
|
||||||
@ -245,7 +396,19 @@ proc start*(cfg: StrawBossConfig): void =
|
|||||||
checkAuth(); if not authed: return true
|
checkAuth(); if not authed: return true
|
||||||
|
|
||||||
try: resp($cfg.getBuildStatus(@"projectName", @"stepName", @"buildRef"), JSON)
|
try: resp($cfg.getBuildStatus(@"projectName", @"stepName", @"buildRef"), JSON)
|
||||||
except: resp(Http404, makeJsonResp(Http404, getCurrentExceptionMsg()), JSON)
|
except:
|
||||||
|
try: raise getCurrentException()
|
||||||
|
except NotFoundException: response.buildJson(Http404, getCurrentExceptionMsg())
|
||||||
|
except:
|
||||||
|
response.build500Json(getCurrentException(), "unable to load the build state for " &
|
||||||
|
@"projectName" & ":" & @"stepName" & "@" & @"buildRef")
|
||||||
|
return true
|
||||||
|
|
||||||
|
#get "/project/@projectName/step/@stepName/status/@buildRef.svg":
|
||||||
|
## Get an image representing the status of a build
|
||||||
|
|
||||||
|
## TODO: how do we want to handle auth for this? Unlike
|
||||||
|
#checkAuth(): if not authed: return true
|
||||||
|
|
||||||
post "/project/@projectName/step/@stepName/run/@buildRef?":
|
post "/project/@projectName/step/@stepName/run/@buildRef?":
|
||||||
# Kick off a run
|
# Kick off a run
|
||||||
@ -271,26 +434,26 @@ proc start*(cfg: StrawBossConfig): void =
|
|||||||
id: runRequest.runId,
|
id: runRequest.runId,
|
||||||
request: runRequest,
|
request: runRequest,
|
||||||
status: status), JSON)
|
status: status), JSON)
|
||||||
except: resp(Http404, makeJsonResp(Http404, getCurrentExceptionMsg()), JSON)
|
except:
|
||||||
|
try: raise getCurrentException()
|
||||||
get "/service/debug/ping":
|
except NotFoundException:
|
||||||
if not cfg.debug: resp(Http404, makeJsonResp(Http404), JSON)
|
response.buildJson(Http404, getCurrentExceptionMsg())
|
||||||
else: resp($(%"pong"), JSON)
|
except: response.buildJson(Http400, getCurrentExceptionMsg())
|
||||||
|
return true
|
||||||
|
|
||||||
post "/service/debug/stop":
|
post "/service/debug/stop":
|
||||||
if not cfg.debug: resp(Http404, makeJsonResp(Http404), JSON)
|
if not cfg.debug: response.buildJson(Http404); return true
|
||||||
else:
|
else:
|
||||||
let shutdownFut = sleepAsync(100)
|
let shutdownFut = sleepAsync(100)
|
||||||
shutdownFut.callback = proc(): void = complete(stopFuture)
|
shutdownFut.callback = proc(): void = complete(stopFuture)
|
||||||
resp($(%"shutting down"), JSON)
|
resp($(%"shutting down"), JSON)
|
||||||
|
|
||||||
#[
|
|
||||||
get re".*":
|
get re".*":
|
||||||
resp(Http404, makeJsonResp(Http404), JSON)
|
response.buildJson(Http404); return true
|
||||||
|
|
||||||
post re".*":
|
post re".*":
|
||||||
resp(Http404, makeJsonResp(Http404), JSON)
|
response.buildJson(Http404); return true
|
||||||
]#
|
|
||||||
|
|
||||||
proc performMaintenance(cfg: StrawBossConfig): void =
|
proc performMaintenance(cfg: StrawBossConfig): void =
|
||||||
# Prune workers
|
# Prune workers
|
||||||
|
9
src/main/systemd/strawboss.service
Normal file
9
src/main/systemd/strawboss.service
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=StrawBoss build server.
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=strawboss
|
||||||
|
WorkingDirectory=/home/strawboss
|
||||||
|
ExecStart=/home/strawboss/strawboss
|
||||||
|
Restart=on-failure
|
@ -135,12 +135,12 @@ suite "strawboss server":
|
|||||||
let completedRun = http.waitForBuild(apiBase, testProjName, $queuedRun.id)
|
let completedRun = http.waitForBuild(apiBase, testProjName, $queuedRun.id)
|
||||||
|
|
||||||
# there should be successful status files for both the build and test steps
|
# there should be successful status files for both the build and test steps
|
||||||
for stepName in ["build", "test"]:
|
for step in [("build", BuildState.stepComplete), ("test", BuildState.complete)]:
|
||||||
let statusFile = tempBuildDataDir & "/" & testProjName & "/status/" & stepName & "/0.2.1.json"
|
let statusFile = tempBuildDataDir & "/" & testProjName & "/status/" & step[0] & "/0.2.1.json"
|
||||||
check fileExists(statusFile)
|
check fileExists(statusFile)
|
||||||
|
|
||||||
let status = loadBuildStatus(statusFile)
|
let status = loadBuildStatus(statusFile)
|
||||||
check status.state == BuildState.complete
|
check status.state == step[1]
|
||||||
|
|
||||||
#test "already completed steps should not be rebuilt":
|
#test "already completed steps should not be rebuilt":
|
||||||
# let http = newAuthenticatedHttpClient(apibase, "bob@builder.com", "password")
|
# let http = newAuthenticatedHttpClient(apibase, "bob@builder.com", "password")
|
||||||
|
@ -41,7 +41,7 @@ suite "strawboss server":
|
|||||||
check fromJWT(cfg, tok) == session
|
check fromJWT(cfg, tok) == session
|
||||||
|
|
||||||
test "ping":
|
test "ping":
|
||||||
let resp = http.get(apiBase & "/service/debug/ping")
|
let resp = http.get(apiBase & "/ping")
|
||||||
check:
|
check:
|
||||||
resp.status.startsWith("200")
|
resp.status.startsWith("200")
|
||||||
resp.body == "\"pong\""
|
resp.body == "\"pong\""
|
||||||
|
20
src/util/bash/client.sh
Executable file
20
src/util/bash/client.sh
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
host="${STRAWBOSS_HOST:-localhost:8180}"
|
||||||
|
if [ $# -eq 1 ]; then
|
||||||
|
url="$1"
|
||||||
|
method="GET"
|
||||||
|
data=""
|
||||||
|
elif [ $# -eq 2 ]; then
|
||||||
|
method="$1"
|
||||||
|
url="$2"
|
||||||
|
data=""
|
||||||
|
else
|
||||||
|
method="$1"
|
||||||
|
url="$2"
|
||||||
|
data="$3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -X "$method" -H "Authorization: Bearer $(cat token.txt)" "http://${host}/api/$url" -d "$data"
|
||||||
|
echo ""
|
||||||
|
#echo "curl -X \"$method\" -H \"Authorization: Bearer $(cat token.txt)\" \"localhost:8180/api/$url\" | jq . "
|
@ -5,6 +5,7 @@
|
|||||||
"authSecret": "change me",
|
"authSecret": "change me",
|
||||||
"pwdCost": 11,
|
"pwdCost": 11,
|
||||||
"maintenancePeriod": 5000,
|
"maintenancePeriod": 5000,
|
||||||
|
"logLevel": "info",
|
||||||
"projects": [
|
"projects": [
|
||||||
{ "name": "new-life-intro-band",
|
{ "name": "new-life-intro-band",
|
||||||
"repo": "/home/jdb/projects/new-life-introductory-band" },
|
"repo": "/home/jdb/projects/new-life-introductory-band" },
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
bin = @["strawboss"]
|
bin = @["strawboss"]
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "My personal continious integration worker."
|
description = "My personal continious integration worker."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@ -12,12 +12,18 @@ srcDir = "src/main/nim"
|
|||||||
requires @["nim >= 0.16.1", "docopt >= 0.6.5", "isaac >= 0.1.2", "tempfile", "jester", "bcrypt",
|
requires @["nim >= 0.16.1", "docopt >= 0.6.5", "isaac >= 0.1.2", "tempfile", "jester", "bcrypt",
|
||||||
"untar", "uuids"]
|
"untar", "uuids"]
|
||||||
|
|
||||||
requires "https://github.com/yglukhov/nim-jwt"
|
# Hacky to point to a specific hash. But there is some bug building in the
|
||||||
|
# docker image we use to build the project with the next version. It adds an
|
||||||
|
# ifdef branch to support libssl 1.1 but for some reason that ifdef is set
|
||||||
|
# wrong and it tries to build against the 1.1 API even though the image only
|
||||||
|
# has the 1.0 API. I'm crossing my fingers and hoping that our base image
|
||||||
|
# supports libssl 1.1 before I need to update this library.
|
||||||
|
requires "https://github.com/yglukhov/nim-jwt#549aa1eb13b8ddc0c6861d15cc2cc5b52bcbef01"
|
||||||
|
|
||||||
requires "https://git.jdb-labs.com/jdb/nim-lang-utils.git >= 0.3.0"
|
requires "https://git.jdb-labs.com/jdb/nim-lang-utils.git >= 0.3.0"
|
||||||
requires "https://git.jdb-labs.com/jdb/nim-cli-utils.git >= 0.3.1"
|
requires "https://git.jdb-labs.com/jdb/nim-cli-utils.git >= 0.3.1"
|
||||||
|
|
||||||
# Tasks
|
# Tasks
|
||||||
#
|
|
||||||
task functest, "Runs the functional test suite.":
|
task functest, "Runs the functional test suite.":
|
||||||
exec "nimble build"
|
exec "nimble build"
|
||||||
exec "nim c -r src/test/nim/run_functional_tests.nim"
|
exec "nim c -r src/test/nim/run_functional_tests.nim"
|
||||||
|
@ -1,12 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "strawboss",
|
"name": "strawboss",
|
||||||
"steps": {
|
"steps": {
|
||||||
"build": {
|
"compile": {
|
||||||
"artifacts": ["strawboss"],
|
"artifacts": ["strawboss"],
|
||||||
"stepCmd": "nimble build"
|
"stepCmd": "docker run -v `pwd`:/usr/src/strawboss -w /usr/src/strawboss jdbernard/nim:0.17.2 nimble build"
|
||||||
},
|
},
|
||||||
"test": { "depends": ["unittest", "functest"] },
|
"unittest": {
|
||||||
"functest": { "stepCmd": "nimble functest" },
|
"depends": ["compile"],
|
||||||
"unittest": { "stepCmd": "nimble unittest" }
|
"stepCmd": "docker run -v `pwd`:/usr/src/strawboss -v $compile_DIR:/usr/build/strawboss -w /usr/src/strawboss -i jdbernard/nim:0.17.2 /bin/bash",
|
||||||
|
"cmdInput": [
|
||||||
|
"cp /usr/build/strawboss/strawboss .",
|
||||||
|
"nimble install --depsOnly",
|
||||||
|
"nim c -r src/test/nim/run_unit_tests"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"functest": {
|
||||||
|
"depends": ["compile"],
|
||||||
|
"stepCmd": "docker run -v `pwd`:/usr/src/strawboss -v $compile_DIR:/usr/build/strawboss -w /usr/src/strawboss -i jdbernard/nim:0.17.2 /bin/bash",
|
||||||
|
"cmdInput": [
|
||||||
|
"cp /usr/build/strawboss/strawboss .",
|
||||||
|
"nimble install --depsOnly",
|
||||||
|
"nim c -r src/test/nim/run_functional_tests"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"artifacts": ["strawboss-$VERSION.zip"],
|
||||||
|
"depends": ["compile", "unittest", "functest"],
|
||||||
|
"stepCmd": "docker run -v `pwd`:/usr/src/strawboss -v $compile_DIR:/usr/build/strawboss -w /usr/src/strawboss -i jdbernard/nim:0.17.2 /bin/bash",
|
||||||
|
"cmdInput": [
|
||||||
|
"cp /usr/build/strawboss/strawboss .",
|
||||||
|
"zip strawboss-$VERSION.zip strawboss strawboss.config.json example.json src/main/systemd/strawboss.service"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user