diff --git a/api.rst b/api.rst index 137eaf8..e9e3d86 100644 --- a/api.rst +++ b/api.rst @@ -1,9 +1,16 @@ ✓ 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 - POST /api/projects -- create a new project -- GET /api/project/ -- return detailed project record (include steps) +* GET /api/project/ -- return detailed project record (include steps) - GET /api/project//active -- return detailed information about all currently running runs - GET /api/project// -- return detailed step information (include runs) -- POST /api/project///run/ -- kick off a run +* POST /api/project///run/ -- kick off a run - GET /api/project///run/ -- return detailed run information + + +Legend: + ✓ implemented with passing tests + * implemented, needs testing + - not implemented diff --git a/src/main/nim/strawbosspkg/configuration.nim b/src/main/nim/strawbosspkg/configuration.nim index a188168..1f4d76b 100644 --- a/src/main/nim/strawbosspkg/configuration.nim +++ b/src/main/nim/strawbosspkg/configuration.nim @@ -79,7 +79,7 @@ proc getOrFail(n: JsonNode, key: string, objName: string = ""): JsonNode = 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("") @@ -186,13 +186,28 @@ proc `%`*(req: RunRequest): JsonNode = "workspaceDir": req.workspaceDir, "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 `$`*(req: RunRequest): string = result = pretty(%req) 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? #proc `==`*(a, b: ProjectDef): bool = - + template shallowEquals(a, b: RootObj): bool = if type(a) != type(b): return false var anyB = toAny(b) diff --git a/src/main/nim/strawbosspkg/core.nim b/src/main/nim/strawbosspkg/core.nim index ca633ef..a5ab14d 100644 --- a/src/main/nim/strawbosspkg/core.nim +++ b/src/main/nim/strawbosspkg/core.nim @@ -140,7 +140,10 @@ proc runStep*(wksp: Workspace, step: Step) = let artifactPath = a.resolveEnvVars(wksp.env) let artifactName = artifactPath[(artifactPath.rfind("/")+1)..^1] 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, wksp.artifactsDir & "/" & artifactName) except: @@ -205,6 +208,13 @@ proc runStep*(cfg: StrawBossConfig, req: RunRequest, "cloning project repo and preparing to run '" & req.stepName & "'") 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 if not wksp.project.steps.hasKey(req.stepName): raiseEx "no step name '" & req.stepName & "' for " & req.projectName diff --git a/src/main/nim/strawbosspkg/server.nim b/src/main/nim/strawbosspkg/server.nim index bdcd253..c4bcd49 100644 --- a/src/main/nim/strawbosspkg/server.nim +++ b/src/main/nim/strawbosspkg/server.nim @@ -1,5 +1,5 @@ -import asyncdispatch, bcrypt, jester, json, jwt, os, osproc, sequtils, - strutils, tempfile, times, unittest +import algorithm, asyncdispatch, bcrypt, jester, json, jwt, os, osproc, + sequtils, strutils, tempfile, times, unittest import logging import ./configuration, ./core, private/util @@ -143,7 +143,60 @@ proc start*(cfg: StrawBossConfig): void = resp(Http200, $(%*{ "username": session.user.name }), JSON) 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?": # Kick off a run @@ -159,4 +212,12 @@ proc start*(cfg: StrawBossConfig): void = callSoon(proc(): void = complete(stopFuture)) resp($(%*"shutting down"), JSON) + #[ + get re".*": + resp(Http404, makeJsonResp(Http404), JSON) + + post re".*": + resp(Http404, makeJsonResp(Http404), JSON) + ]# + waitFor(stopFuture) diff --git a/src/test/nim/tconfiguration.nim b/src/test/nim/tconfiguration.nim index 423b776..d430288 100644 --- a/src/test/nim/tconfiguration.nim +++ b/src/test/nim/tconfiguration.nim @@ -107,6 +107,10 @@ suite "load and save configuration objects": sameContents(pc.steps["test"].expectedEnv, @[]) sameContents(pc.steps["test"].cmdInput, @[]) + test "StrawBossConfig to string": + # TODO + check false + test "loadBuildStatus": let st = loadBuildStatus("src/test/json/test-status.json") diff --git a/src/test/nim/tserver.nim b/src/test/nim/tserver.nim index 73e24c6..867dbc5 100644 --- a/src/test/nim/tserver.nim +++ b/src/test/nim/tserver.nim @@ -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 ../../main/nim/strawbosspkg/configuration import ../../main/nim/strawbosspkg/server import ../../main/nim/strawbosspkg/private/util -import strtabs - # test helpers proc newAuthenticatedHttpClient(apiBase, uname, pwd: string): HttpClient = result = newHttpClient() @@ -13,20 +14,20 @@ proc newAuthenticatedHttpClient(apiBase, uname, pwd: string): HttpClient = assert authResp.status.startsWith("200") 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 - let cfgFilePath = "src/test/json/strawboss.config.json" - let cfg = loadStrawBossConfig(cfgFilePath) - discard startProcess("./strawboss", ".", @["serve", "-c", cfgFilePath], loadEnv(), {poUsePath}) 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 sleep(100) @@ -88,5 +89,25 @@ suite "strawboss server can...": check sameContents(projects, cfg.projects) # suite tear-down - try: discard http.post(apiBase & "/service/debug/stop") - except: discard "" + discard newAsyncHttpClient().post(apiBase & "/service/debug/stop") + +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")