diff --git a/api.rst b/api.rst index 0e4bd0e..137eaf8 100644 --- a/api.rst +++ b/api.rst @@ -1,5 +1,5 @@ ✓ GET /api/ping -- POST /api/auth-token +- GET /api/auth-token ✓ GET /api/projects -- return project summaries - POST /api/projects -- create a new project - GET /api/project/ -- return detailed project record (include steps) diff --git a/src/main/nim/strawboss.nim b/src/main/nim/strawboss.nim index f16b93d..dc705f9 100644 --- a/src/main/nim/strawboss.nim +++ b/src/main/nim/strawboss.nim @@ -1,9 +1,9 @@ import docopt, os, sequtils, tempfile -import strawboss/private/util -import strawboss/configuration -import strawboss/core -import strawboss/server +import strawbosspkg/private/util +import strawbosspkg/configuration +import strawbosspkg/core +import strawbosspkg/server let SB_VER = "0.2.0" diff --git a/src/main/nim/strawboss/configuration.nim b/src/main/nim/strawbosspkg/configuration.nim similarity index 86% rename from src/main/nim/strawboss/configuration.nim rename to src/main/nim/strawbosspkg/configuration.nim index d548200..b04b494 100644 --- a/src/main/nim/strawboss/configuration.nim +++ b/src/main/nim/strawbosspkg/configuration.nim @@ -12,7 +12,7 @@ type artifacts*, cmdInput*, depends*, expectedEnv*: seq[string] dontSkip*: bool - ProjectCfg* = object + ProjectConfig* = object name*: string versionCmd*: string steps*: Table[string, Step] @@ -37,15 +37,31 @@ type projects*: seq[ProjectDef] 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 # 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] @@ -84,7 +100,7 @@ proc loadStrawBossConfig*(cfgFile: string): StrawBossConfig = projects: projectDefs, users: users) -proc loadProjectConfig*(cfgFile: string): ProjectCfg = +proc loadProjectConfig*(cfgFile: string): ProjectConfig = if not existsFile(cfgFile): raiseEx "project config file not found: " & cfgFile @@ -103,12 +119,12 @@ proc loadProjectConfig*(cfgFile: string): ProjectCfg = 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").getStr("false") != "false") + 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 = ProjectCfg( + result = ProjectConfig( name: jsonCfg.getOrFail("name", "project configuration").getStr, versionCmd: jsonCfg.getIfExists("versionCmd").getStr("git describe --tags --always"), steps: steps) @@ -121,6 +137,7 @@ proc loadBuildStatus*(statusFile: string): 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) diff --git a/src/main/nim/strawboss/core.nim b/src/main/nim/strawbosspkg/core.nim similarity index 99% rename from src/main/nim/strawboss/core.nim rename to src/main/nim/strawbosspkg/core.nim index f3096e4..6deb49c 100644 --- a/src/main/nim/strawboss/core.nim +++ b/src/main/nim/strawbosspkg/core.nim @@ -13,7 +13,7 @@ type env*: StringTableRef ## environment variables for all build processes openedFiles*: seq[File] ## all files that we have opened that need to be closed outputHandler*: HandleProcMsgCB ## handler for process output - project*: ProjectCfg ## the project configuration + project*: ProjectConfig ## the project configuration projectDef*: ProjectDef ## the StrawBoss project definition status*: BuildStatus ## the current status of the build statusFile*: string ## absolute path to the build status file @@ -188,7 +188,7 @@ proc runStep*(cfg: StrawBossConfig, req: RunRequest, env: env, openedFiles: @[stdoutFile, stderrFile], outputHandler: combineProcMsgHandlers(outputHandler, logFilesOH), - project: ProjectCfg(), + project: ProjectConfig(), projectDef: matching[0], status: result, statusFile: req.workspaceDir & "/" & "status.json", diff --git a/src/main/nim/strawboss/private/util.nim b/src/main/nim/strawbosspkg/private/util.nim similarity index 100% rename from src/main/nim/strawboss/private/util.nim rename to src/main/nim/strawbosspkg/private/util.nim diff --git a/src/main/nim/strawboss/server.nim b/src/main/nim/strawbosspkg/server.nim similarity index 69% rename from src/main/nim/strawboss/server.nim rename to src/main/nim/strawbosspkg/server.nim index 100a299..70c9aa0 100644 --- a/src/main/nim/strawboss/server.nim +++ b/src/main/nim/strawbosspkg/server.nim @@ -1,4 +1,4 @@ -import asyncdispatch, jester, json, jwt, osproc, sequtils, tempfile, times +import asyncdispatch, bcrypt, jester, json, jwt, osproc, sequtils, tempfile, times import ./configuration, ./core, private/util @@ -15,11 +15,13 @@ type issuedAt*, expires*: TimeInfo const ISO_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" +const BCRYPT_ROUNDS = 16 -proc makeJsonResp(status: HttpCode): string = +proc makeJsonResp(status: HttpCode, details: string = ""): string = result = $(%* { "statusCode": status.int, - "status": $status + "status": $status, + "details": details }) proc newSession(user: UserRef): Session = @@ -61,7 +63,7 @@ proc extractSession(cfg: StrawBossConfig, request: Request): Session = template requireAuth() = var session {.inject.}: Session try: session = extractSession(givenCfg, request) - except: resp Http401, makeJsonResp(Http401), "application/json" + except: resp(Http401, makeJsonResp(Http401), "application/json") proc spawnWorker(req: RunRequest): Worker = let dir = mkdtemp() @@ -77,15 +79,36 @@ proc start*(givenCfg: StrawBossConfig): void = routes: get "/api/ping": - resp $(%*"pong"), "application/json" + resp($(%*"pong"), "application/json") get "/api/auth-token": - resp Http501, makeJsonResp(Http501), "application/json" + resp(Http501, makeJsonResp(Http501), "application/json") get "/api/projects": requireAuth() - #let projectDefs: seq[ProjectDef] = givenCfg.projects.mapIt(it) - resp $(%(givenCfg.projects)), "application/json" + resp($(%(givenCfg.projects)), "application/json") + + get "/api/auth-token": + var username, pwd: string + try: + username = @"username" + pwd = @"password" + except: resp(Http401, makeJsonResp(Http401, "fields 'username' and 'password' required")) + + let users = givenCfg.users.filterIt(it.name == username) + if users.len != 1: + resp(Http401, makeJsonResp(Http401, "invalid username or password")) + + let user = users[0] + + # generate salt + let salt = genSalt(BCRYPT_ROUNDS) + + # bcrypt + let hashedPwd = hash(pwd, salt) + stdout.writeLine "Hashed pwd is " & $hashedPwd + + resp(Http501, makeJsonResp(Http501)) post "/api/project/@projectName/@stepName/run/@buildRef?": workers.add(spawnWorker(RunRequest( diff --git a/src/test/json/strawboss.config.json b/src/test/json/strawboss.config.json new file mode 100644 index 0000000..7935b0b --- /dev/null +++ b/src/test/json/strawboss.config.json @@ -0,0 +1,17 @@ +{ + "artifactsRepo": "artifacts", + "users": [ + { "name": "bob@builder.com", "hashedPwd": "testvalue" }, + { "name": "sam@sousa.com", "hashedPwd": "testvalue" } + ], + "authSecret": "change me", + "projects": [ + { "name": "test-project-1", + "repo": "/non-existent/dir", + "cfgFilePath": "strawhat.json", + "defaultBranch": "deploy", + "envVars": { "VAR1": "value" } + }, + { "name": "test-strawboss", + "repo": "https://git.jdb-labs.com:jdb/test-strawboss.git" } ] +} diff --git a/src/test/json/test-project-1.config.json b/src/test/json/test-project-1.config.json new file mode 100644 index 0000000..b03f137 --- /dev/null +++ b/src/test/json/test-project-1.config.json @@ -0,0 +1,16 @@ +{ + "name": "test-project-1", + "versionCmd": "git describe --all --always", + "steps": { + "build": { + "depends": ["test"], + "workingDir": "dir1", + "stepCmd": "cust-build", + "artifacts": ["bin1", "doc1"], + "expectedEnv": ["VAR1"], + "dontSkip": true, + "cmdInput": ["test", "this"] + }, + "test": { } + } +} diff --git a/src/test/json/test-status.json b/src/test/json/test-status.json new file mode 100644 index 0000000..da2a735 --- /dev/null +++ b/src/test/json/test-status.json @@ -0,0 +1,4 @@ +{ + "state": "failed", + "details": "some very good reason" +} diff --git a/src/test/nim/strawbosspkg/tconfiguration b/src/test/nim/strawbosspkg/tconfiguration new file mode 100644 index 0000000..2cc5094 Binary files /dev/null and b/src/test/nim/strawbosspkg/tconfiguration differ diff --git a/src/test/nim/strawbosspkg/tconfiguration.nim b/src/test/nim/strawbosspkg/tconfiguration.nim new file mode 100644 index 0000000..225eaa7 --- /dev/null +++ b/src/test/nim/strawbosspkg/tconfiguration.nim @@ -0,0 +1,62 @@ +import strtabs, tables, unittest +import ./testutil +import ../../../main/nim/strawbosspkg/configuration + +suite "load and save configuration objects": + + test "loadStrawBossConfig": + let cfg = loadStrawBossConfig("src/test/json/strawboss.config.json") + let expectedUsers = @[UserRef(name: "bob@builder.com", hashedPwd: "testvalue"), + UserRef(name: "sam@sousa.com", hashedPwd: "testvalue")] + let expectedProjects = @[ + ProjectDef(name: "test-project-1", + repo: "/non-existent/dir", + defaultBranch: "deploy", + cfgFilePath: "strawhat.json", + envVars: newStringTable("VAR1", "value", modeCaseSensitive)), + ProjectDef(name: "test-strawboss", + repo: "https://git.jdb-labs.com:jdb/test-strawboss.git", + defaultBranch: "master", + cfgFilePath: "strawboss.json", + envVars: newStringTable(modeCaseSensitive))] + + check: + cfg.artifactsRepo == "artifacts" + cfg.authSecret == "change me" + sameContents(expectedUsers, cfg.users) + sameContents(expectedProjects, cfg.projects) + + test "loadProjectConfig": + let pc = loadProjectConfig("src/test/json/test-project-1.config.json") + + check: + pc.name == "test-project-1" + pc.versionCmd == "git describe --all --always" + pc.steps.len == 2 + + # Explicitly set properties + pc.steps["build"].name == "build" + pc.steps["build"].dontSkip == true + pc.steps["build"].stepCmd == "cust-build" + pc.steps["build"].workingDir == "dir1" + sameContents(pc.steps["build"].artifacts, @["bin1", "doc1"]) + sameContents(pc.steps["build"].depends, @["test"]) + sameContents(pc.steps["build"].expectedEnv, @["VAR1"]) + sameContents(pc.steps["build"].cmdInput, @["test", "this"]) + + # Step with defaulted properties + pc.steps["test"].name == "test" + pc.steps["test"].dontSkip == false + pc.steps["test"].stepCmd == "sh" + pc.steps["test"].workingDir == "." + sameContents(pc.steps["test"].artifacts, @[]) + sameContents(pc.steps["test"].depends, @[]) + sameContents(pc.steps["test"].expectedEnv, @[]) + sameContents(pc.steps["test"].cmdInput, @[]) + + test "loadBuildStatus": + let st = loadBuildStatus("src/test/json/test-status.json") + + check: + st.state == "failed" + st.details == "some very good reason" diff --git a/src/test/nim/strawbosspkg/testutil.nim b/src/test/nim/strawbosspkg/testutil.nim new file mode 100644 index 0000000..603d69c --- /dev/null +++ b/src/test/nim/strawbosspkg/testutil.nim @@ -0,0 +1,52 @@ +import unittest, sequtils + +proc sameContents*[T](a1, a2: openArray[T]): bool = + # Answers the question: do these two arrays contain the same contents, + # regardless of their order? + if a1.len != a2.len: return false + for a in a1: + if not a2.anyIt(a == it): return false + return true + +type + SimpleObj* = object + strVal*: string + + TestObj = object + strVal*: string + intVal*: int + objVal*: SimpleObj + +suite "testutil": + + test "sameContents": + let a1 = [1, 2, 3, 4] + let a2 = [3, 2, 4, 1] + let a3 = [1, 2, 3, 5] + let b1 = ["this", "is", "a", "test"] + let b2 = ["a", "test", "this", "is"] + let b3 = ["a", "negative", "test", "this", "is"] + let c1 = [TestObj(strVal: "a", intVal: 1, objVal: SimpleObj(strVal: "innerA")), + TestObj(strVal: "b", intVal: 2, objVal: SimpleObj(strVal: "innerB"))] + let c2 = [c1[1], c1[0]] + + check: + sameContents(a1, a2) + sameContents(b2, b1) + sameContents(c1, c2) + sameContents(a1, a3) == false + sameContents(b1, b3) == false + + test "sameContents (seq)": + let a1 = @[1, 2, 3, 4] + let a2 = @[3, 2, 4, 1] + + check: + sameContents(a1, a2) + + test "sameContents (cross-type)": + let a1 = @[1, 2, 3, 4] + let a2 = [3, 2, 4, 1] + + check: + sameContents(a1, a2) diff --git a/strawboss.nimble b/strawboss.nimble index 2538f9d..d47e6d8 100644 --- a/strawboss.nimble +++ b/strawboss.nimble @@ -9,6 +9,6 @@ srcDir = "src/main/nim" # Dependencies -requires @["nim >= 0.16.1", "docopt >= 0.1.0", "tempfile", "jester"] +requires @["nim >= 0.16.1", "docopt >= 0.1.0", "tempfile", "jester", "bcrypt"] requires "https://github.com/yglukhov/nim-jwt"