Implemented GET on /projects/<proj-id> and started unit tests.

This commit is contained in:
Jonathan Bernard 2017-04-25 12:57:13 -05:00
parent e547ecd607
commit 1e2af48892
6 changed files with 141 additions and 23 deletions

13
api.rst
View File

@ -1,9 +1,16 @@
✓ GET /api/ping ✓ GET /api/ping
- GET /api/auth-token ✓ GET /api/auth-token
✓ GET /api/verify-auth -- returns 200 or 401 depend on validity of the provided auth
✓ GET /api/projects -- return project summaries ✓ GET /api/projects -- return project summaries
- POST /api/projects -- create a new project - POST /api/projects -- create a new project
- GET /api/project/<proj-id> -- return detailed project record (include steps) * GET /api/project/<proj-id> -- return detailed project record (include steps)
- GET /api/project/<proj-id>/active -- return detailed information about all currently running runs - GET /api/project/<proj-id>/active -- return detailed information about all currently running runs
- GET /api/project/<proj-id>/<step-id> -- return detailed step information (include runs) - GET /api/project/<proj-id>/<step-id> -- return detailed step information (include runs)
- POST /api/project/<proj-id>/<step-id>/run/<ref> -- kick off a run * POST /api/project/<proj-id>/<step-id>/run/<ref> -- kick off a run
- GET /api/project/<proj-id>/<step-id>/run/<ref> -- return detailed run information - GET /api/project/<proj-id>/<step-id>/run/<ref> -- return detailed run information
Legend:
✓ implemented with passing tests
* implemented, needs testing
- not implemented

View File

@ -79,7 +79,7 @@ proc getOrFail(n: JsonNode, key: string, objName: string = ""): JsonNode =
return n[key] return n[key]
# Configuration parsing code # Configuration parsing code
proc parseProjectDef*(pJson: JsonNode): ProjectDef = proc parseProjectDef*(pJson: JsonNode): ProjectDef =
var envVars = newStringTable(modeCaseSensitive) var envVars = newStringTable(modeCaseSensitive)
for k, v in pJson.getIfExists("envVars").getFields: envVars[k] = v.getStr("") for k, v in pJson.getIfExists("envVars").getFields: envVars[k] = v.getStr("")
@ -186,13 +186,28 @@ proc `%`*(req: RunRequest): JsonNode =
"workspaceDir": req.workspaceDir, "workspaceDir": req.workspaceDir,
"forceRebuild": req.forceRebuild } "forceRebuild": req.forceRebuild }
proc `%`*(user: User): JsonNode =
result = %* {
"name": user.name,
"hashedPwd": user.hashedPwd }
proc `%`*(cfg: StrawBossConfig): JsonNode =
result = %* {
"artifactsRepo": cfg.artifactsRepo,
"authSecret": cfg.authSecret,
"debug": cfg.debug,
"projects": %cfg.projects,
"pwdCost": cfg.pwdCost,
"users": %cfg.users }
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)
# TODO: maybe a macro for more general-purpose, shallow object comparison? # TODO: maybe a macro for more general-purpose, shallow object comparison?
#proc `==`*(a, b: ProjectDef): bool = #proc `==`*(a, b: ProjectDef): bool =
template shallowEquals(a, b: RootObj): bool = template shallowEquals(a, b: RootObj): bool =
if type(a) != type(b): return false if type(a) != type(b): return false
var anyB = toAny(b) var anyB = toAny(b)

View File

@ -140,7 +140,10 @@ proc runStep*(wksp: Workspace, step: Step) =
let artifactPath = a.resolveEnvVars(wksp.env) let artifactPath = a.resolveEnvVars(wksp.env)
let artifactName = artifactPath[(artifactPath.rfind("/")+1)..^1] let artifactName = artifactPath[(artifactPath.rfind("/")+1)..^1]
try: try:
wksp.outputHandler.sendMsg "copy " & wksp.dir & "/repo/" & step.workingDir & "/" & artifactPath & " -> " & wksp.artifactsDir & "/" & artifactName wksp.outputHandler.sendMsg "copy " & wksp.dir & "/repo/" &
step.workingDir & "/" & artifactPath & " -> " &
wksp.artifactsDir & "/" & artifactName
copyFile(wksp.dir & "/repo/" & step.workingDir & "/" & artifactPath, copyFile(wksp.dir & "/repo/" & step.workingDir & "/" & artifactPath,
wksp.artifactsDir & "/" & artifactName) wksp.artifactsDir & "/" & artifactName)
except: except:
@ -205,6 +208,13 @@ proc runStep*(cfg: StrawBossConfig, req: RunRequest,
"cloning project repo and preparing to run '" & req.stepName & "'") "cloning project repo and preparing to run '" & req.stepName & "'")
wksp.setupProject() wksp.setupProject()
# Update our cache of project configurations by copying the configuration
# file to our artifacts directory.
copyFile(
wksp.dir & "/repo/" & wksp.projectDef.cfgFilePath,
cfg.artifactsRepo & "/" & wksp.project.name & "/configuration." &
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):
raiseEx "no step name '" & req.stepName & "' for " & req.projectName raiseEx "no step name '" & req.stepName & "' for " & req.projectName

View File

@ -1,5 +1,5 @@
import asyncdispatch, bcrypt, jester, json, jwt, os, osproc, sequtils, import algorithm, asyncdispatch, bcrypt, jester, json, jwt, os, osproc,
strutils, tempfile, times, unittest sequtils, strutils, tempfile, times, unittest
import logging import logging
import ./configuration, ./core, private/util import ./configuration, ./core, private/util
@ -143,7 +143,60 @@ proc start*(cfg: StrawBossConfig): void =
resp(Http200, $(%*{ "username": session.user.name }), JSON) resp(Http200, $(%*{ "username": session.user.name }), JSON)
get "/projects": withSession: get "/projects": withSession:
resp($(%(givenCfg.projects)), "application/json") # List project summaries (ProjectDefs only)
resp($(%(cfg.projects)), JSON)
post "/projects": withSession:
# Create a new project definition
resp(Http501, makeJsonResp(Http501), JSON)
get "/project/@projectName/@version?": withSession:
## Get a detailed project record including step definitions (ProjectConfig).
# Make sure we know about that project
var project: ProjectDef
try: project = cfg.findProject(@"projectName")
except: resp(Http404, makeJsonResp(Http404, getCurrentExceptionMsg()), JSON)
# Given version
var cachedFilePath: string
if @"version" != "":
cachedFilePath = cfg.artifactsRepo & "/" & project.name &
"/configuration." & @"version" & ".json"
if not existsFile(cachedFilePath):
resp(Http404,
makeJsonResp(Http404, "I have never built version " & @"version"),
JSON)
# No version requested, use "latest"
else:
let confFilePaths = toSeq(walkFiles("configuration.*.json"))
if confFilePaths.len == 0:
resp(Http404, makeJsonResp(Http404, "I have not built any versions of " & project.name), JSON)
let modTimes = confFilePaths.mapIt(it.getLastModificationTime)
cachedFilePath = sorted(zip(confFilePaths, modTimes),
proc (a, b: tuple): int = cmp(a.b, b.b))[0].a
try: resp(readFile(cachedFilePath), JSON)
except:
debug "Could not serve cached project configuration at: " &
cachedFilePath & "\n\t Reason: " & getCurrentExceptionMsg()
resp(Http500, makeJsonResp(Http500, "could not read cached project configuration"), JSON)
get "/api/project/@projectName/active": withSession:
# List all currently active runs
resp(Http501, makeJsonResp(Http501), JSON)
get "/api/project/@projectName/@stepName": withSession:
# Get step details including runs.
resp(Http501, makeJsonResp(Http501), JSON)
get "/api/project/@projectName/@stepName/run/@buildRef": withSession:
# Get detailed information about a run
resp(Http501, makeJsonResp(Http501), JSON)
post "/project/@projectName/@stepName/run/@buildRef?": post "/project/@projectName/@stepName/run/@buildRef?":
# Kick off a run # Kick off a run
@ -159,4 +212,12 @@ proc start*(cfg: StrawBossConfig): void =
callSoon(proc(): void = complete(stopFuture)) callSoon(proc(): void = complete(stopFuture))
resp($(%*"shutting down"), JSON) resp($(%*"shutting down"), JSON)
#[
get re".*":
resp(Http404, makeJsonResp(Http404), JSON)
post re".*":
resp(Http404, makeJsonResp(Http404), JSON)
]#
waitFor(stopFuture) waitFor(stopFuture)

View File

@ -107,6 +107,10 @@ suite "load and save configuration objects":
sameContents(pc.steps["test"].expectedEnv, @[]) sameContents(pc.steps["test"].expectedEnv, @[])
sameContents(pc.steps["test"].cmdInput, @[]) sameContents(pc.steps["test"].cmdInput, @[])
test "StrawBossConfig to string":
# TODO
check false
test "loadBuildStatus": test "loadBuildStatus":
let st = loadBuildStatus("src/test/json/test-status.json") let st = loadBuildStatus("src/test/json/test-status.json")

View File

@ -1,11 +1,12 @@
import asyncdispatch, httpclient, json, os, osproc, sequtils, strutils, times, unittest import asyncdispatch, httpclient, json, os, osproc, sequtils, strutils,
tempfile, times, unittest
import logging
import ./testutil import ./testutil
import ../../main/nim/strawbosspkg/configuration import ../../main/nim/strawbosspkg/configuration
import ../../main/nim/strawbosspkg/server import ../../main/nim/strawbosspkg/server
import ../../main/nim/strawbosspkg/private/util import ../../main/nim/strawbosspkg/private/util
import strtabs
# test helpers # test helpers
proc newAuthenticatedHttpClient(apiBase, uname, pwd: string): HttpClient = proc newAuthenticatedHttpClient(apiBase, uname, pwd: string): HttpClient =
result = newHttpClient() result = newHttpClient()
@ -13,20 +14,20 @@ proc newAuthenticatedHttpClient(apiBase, uname, pwd: string): HttpClient =
assert authResp.status.startsWith("200") assert authResp.status.startsWith("200")
result.headers = newHttpHeaders({"Authorization": "Bearer " & parseJson(authResp.body).getStr}) result.headers = newHttpHeaders({"Authorization": "Bearer " & parseJson(authResp.body).getStr})
suite "strawboss server can...": let apiBase = "http://localhost:8180/api"
let cfgFilePath = "src/test/json/strawboss.config.json"
let cfg = loadStrawBossConfig(cfgFilePath)
let testuser = UserRef( # note: needs to correspond to an actual user
name: "bob@builder.com",
hashedPwd: "$2a$11$lVZ9U4optQMhzPh0E9A7Yu6XndXblUF3gCa.zmEvJy4F.4C4718b.")
suite "strawboss server":
# suite setup code # suite setup code
let cfgFilePath = "src/test/json/strawboss.config.json"
let cfg = loadStrawBossConfig(cfgFilePath)
discard startProcess("./strawboss", ".", @["serve", "-c", cfgFilePath], loadEnv(), {poUsePath}) discard startProcess("./strawboss", ".", @["serve", "-c", cfgFilePath], loadEnv(), {poUsePath})
let http = newHttpClient() let http = newHttpClient()
let apiBase = "http://localhost:8180/api"
let testuser = UserRef( # note: needs to correspond to an actual user
name: "bob@builder.com",
hashedPwd: "$2a$11$lVZ9U4optQMhzPh0E9A7Yu6XndXblUF3gCa.zmEvJy4F.4C4718b.")
# give the server time to spin up # give the server time to spin up
sleep(100) sleep(100)
@ -88,5 +89,25 @@ suite "strawboss server can...":
check sameContents(projects, cfg.projects) check sameContents(projects, cfg.projects)
# suite tear-down # suite tear-down
try: discard http.post(apiBase & "/service/debug/stop") discard newAsyncHttpClient().post(apiBase & "/service/debug/stop")
except: discard ""
suite "strawboss server continued":
setup:
let tmpArtifactsDir = mkdtemp()
let (_, tmpCfgPath) = mkstemp()
var newCfg = cfg
newCfg.artifactsRepo = tmpArtifactsDir
writeFile(tmpCfgPath, $newCfg)
discard startProcess("./strawboss", ".", @["serve", "-c", tmpCfgPath], loadEnv(), {poUsePath})
# give the server time to spin up
sleep(100)
teardown:
discard newAsyncHttpClient().post(apiBase & "/service/debug/stop")
test "handle missing project configuration":
let http = newAuthenticatedHttpClient(apibase, "bob@builder.com", "password")
let resp = http.get(apiBase & "/projects/test-project-1")
check resp.status.startsWith("404")