WIP Adding native support for docker.

This commit is contained in:
Jonathan Bernard 2017-12-02 20:47:26 -06:00
parent 0574f0ec6a
commit c827beab5e
4 changed files with 72 additions and 16 deletions

View File

@ -13,17 +13,16 @@ type
complete, failed, queued, rejected, running, setup, stepComplete complete, failed, queued, rejected, running, setup, stepComplete
BuildStatus* = object BuildStatus* = object
runId*, details*: string runId*, details*, version*: string
state*: BuildState state*: BuildState
Step* = object Step* = object
name*, stepCmd*, workingDir*: string containerImage, name*, stepCmd*, workingDir*: string
artifacts*, cmdInput*, depends*, expectedEnv*: seq[string] artifacts*, cmdInput*, depends*, expectedEnv*: seq[string]
dontSkip*: bool dontSkip*: bool
ProjectConfig* = object ProjectConfig* = object
name*: string containerImage*, name*, versionCmd*: string
versionCmd*: string
steps*: Table[string, Step] steps*: Table[string, Step]
ProjectDef* = object ProjectDef* = object
@ -170,14 +169,15 @@ proc loadProjectConfig*(cfgFile: string): ProjectConfig =
var steps = initTable[string, Step]() var steps = initTable[string, Step]()
for sName, pJson in jsonCfg.getOrFail("steps", "project configuration").getFields: for sName, pJson in jsonCfg.getOrFail("steps", "project configuration").getFields:
steps[sName] = Step( steps[sName] = Step(
name: sName, name: sName,
workingDir: pJson.getIfExists("workingDir").getStr("."), workingDir: pJson.getIfExists("workingDir").getStr("."),
stepCmd: pJson.getIfExists("stepCmd").getStr("NOT GIVEN"), stepCmd: pJson.getIfExists("stepCmd").getStr("NOT GIVEN"),
depends: pJson.getIfExists("depends").getElems.mapIt(it.getStr), depends: pJson.getIfExists("depends").getElems.mapIt(it.getStr),
artifacts: pJson.getIfExists("artifacts").getElems.mapIt(it.getStr), artifacts: pJson.getIfExists("artifacts").getElems.mapIt(it.getStr),
cmdInput: pJson.getIfExists("cmdInput").getElems.mapIt(it.getStr), cmdInput: pJson.getIfExists("cmdInput").getElems.mapIt(it.getStr),
expectedEnv: pJson.getIfExists("expectedEnv").getElems.mapIt(it.getStr), expectedEnv: pJson.getIfExists("expectedEnv").getElems.mapIt(it.getStr),
dontSkip: pJson.getIfExists("dontSkip").getBVal(false)) containerImage: pJson.getIfExists("containerImage").getStr(""),
dontSkip: pJson.getIfExists("dontSkip").getBVal(false))
# cmdInput and stepCmd are related, so we have a conditional defaulting. # cmdInput and stepCmd are related, so we have a conditional defaulting.
# Four possibilities: # Four possibilities:
@ -194,6 +194,7 @@ proc loadProjectConfig*(cfgFile: string): ProjectConfig =
result = ProjectConfig( result = ProjectConfig(
name: jsonCfg.getOrFail("name", "project configuration").getStr, name: jsonCfg.getOrFail("name", "project configuration").getStr,
containerImage: jsonCfg.getIfExists("containerImage").getStr(""),
versionCmd: jsonCfg.getIfExists("versionCmd").getStr("git describe --tags --always"), versionCmd: jsonCfg.getIfExists("versionCmd").getStr("git describe --tags --always"),
steps: steps) steps: steps)
@ -259,6 +260,9 @@ proc `%`*(s: Step): JsonNode =
"expectedEnv": s.expectedEnv, "expectedEnv": s.expectedEnv,
"dontSkip": s.dontSkip } "dontSkip": s.dontSkip }
if not s.containerImage.isNullOrEmpty:
result["containerImage"] = %s.containerImage
proc `%`*(p: ProjectConfig): JsonNode = proc `%`*(p: ProjectConfig): JsonNode =
result = %* { result = %* {
"name": p.name, "name": p.name,
@ -268,6 +272,9 @@ proc `%`*(p: ProjectConfig): JsonNode =
for name, step in p.steps: for name, step in p.steps:
result["steps"][name] = %step result["steps"][name] = %step
if not p.containerImage.isNilOrEmpty:
result["containerImage"] = %p.containerImage
proc `%`*(req: RunRequest): JsonNode = proc `%`*(req: RunRequest): JsonNode =
result = %* { result = %* {
"runId": $(req.runId), "runId": $(req.runId),

View File

@ -1,5 +1,5 @@
import cliutils, logging, json, os, ospaths, osproc, sequtils, streams, import cliutils, logging, json, os, ospaths, osproc, sequtils, streams,
strtabs, strutils, tables, times, uuids strtabs, strutils, tables, tempfile, times, uuids
import ./configuration import ./configuration
import nre except toSeq import nre except toSeq
@ -49,7 +49,51 @@ proc newCopy(w: Workspace): Workspace =
step: w.step, step: w.step,
version: w.version) version: w.version)
# Logging wrappers around const WKSP_ROOT = "/strawboss/wksp"
const ARTIFACTS_ROOT = "/strawboss/artifacts"
proc execWithOutput(w: Workspace, cmd, workingDir: string,
args: openarray[string], env: StringTableRef,
options: set[ProcessOption] = {poUsePath},
msgCB: HandleProcMsgCB = nil):
tuple[output: TaintedString, error: TaintedString, exitCode: int]
{.tags: [ExecIOEffect, ReadIOEffect, RootEffect] .} =
# Look for a container image to use
let containerImage =
if not isNilOrEmpty(w.step.containerImage): w.step.containerImage
else: w.project.containerImage
if containerImage.isNilOrEmpty:
return exec(cmd, workingDir, args, env, options, msgCB
var fullEnv = newStringTable(modeCaseSensitive)
for k,v in env: fullEnv[k] = v
var fullArgs = @["run", "-w", WKSP_ROOT, "-v", wksp.dir & ":" & WKSP_ROOT ]
if w.step.name.isNilOrEmpty:
for depStep in step.depends:
fullArgs.add("-v", ARTIFACTS_ROOT / depStep)
fullEnv[depStep & "_DIR"] = ARTIFACTS_DIR / depStep)
let envFile = mkstemp()[0]
writeFile(envFile, toSeq(fullEnv.pairs()).mapIt(it[0] & "=" & it[1]).join("\n"))
fullArgs.add("--env-file", envFile)
fullArgs.add(containerImage)
fullArgs.add(cmd)
echo "Executing docker command: \n\t" & "docker " & $(fullArgs & args)
return execWithOutput("docker", wksp.dir, fullArgs & args, fullEnv, options, msgCB)
proc exec(w: Workspace, cmd, workingDir: string, args: openarray[string],
env: StringTableRef, options: set[ProcessingOption] = {poUsePath},
msgCB: HandleProcMsgCG = nil): int
{.tags: [ExecIOEffect, ReadIOEffect, RootEffect] .} =
return execWithOutput(w, cmd, workingDir, args, env, options, msgCB)[2]
# Utility methods for Workspace activities # Utility methods for Workspace activities
proc sendStatusMsg(oh: HandleProcMsgCB, status: BuildStatus): void = proc sendStatusMsg(oh: HandleProcMsgCB, status: BuildStatus): void =
if not oh.isNil: if not oh.isNil:
@ -89,7 +133,7 @@ proc publishStatus(wksp: Workspace, state: BuildState, details: string): void =
$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)

View File

@ -1,8 +1,10 @@
{ {
"name": "dummy-project", "name": "dummy-project",
"versionCmd": "git describe --all --always", "versionCmd": "git describe --all --always",
"containerImage": "ubuntu",
"steps": { "steps": {
"build": { "build": {
"containerImage": "alpine",
"depends": ["test"], "depends": ["test"],
"workingDir": "dir1", "workingDir": "dir1",
"stepCmd": "cust-build", "stepCmd": "cust-build",

View File

@ -99,6 +99,7 @@ suite "load and save configuration objects":
check: check:
pc.name == "dummy-project" pc.name == "dummy-project"
pc.versionCmd == "git describe --all --always" pc.versionCmd == "git describe --all --always"
pc.containerImage == "ubuntu"
pc.steps.len == 2 pc.steps.len == 2
# Explicitly set properties # Explicitly set properties
@ -106,6 +107,7 @@ suite "load and save configuration objects":
pc.steps["build"].dontSkip == true pc.steps["build"].dontSkip == true
pc.steps["build"].stepCmd == "cust-build" pc.steps["build"].stepCmd == "cust-build"
pc.steps["build"].workingDir == "dir1" pc.steps["build"].workingDir == "dir1"
pc.steps["containerImage"] == "alpine"
sameContents(pc.steps["build"].artifacts, @["bin1", "doc1"]) sameContents(pc.steps["build"].artifacts, @["bin1", "doc1"])
sameContents(pc.steps["build"].depends, @["test"]) sameContents(pc.steps["build"].depends, @["test"])
sameContents(pc.steps["build"].expectedEnv, @["VAR1"]) sameContents(pc.steps["build"].expectedEnv, @["VAR1"])
@ -115,7 +117,8 @@ suite "load and save configuration objects":
pc.steps["test"].name == "test" pc.steps["test"].name == "test"
pc.steps["test"].dontSkip == false pc.steps["test"].dontSkip == false
pc.steps["test"].stepCmd == "true" pc.steps["test"].stepCmd == "true"
pc.steps["test"].workingDir == "." pc.steps["test"].workingDir == ".:
pc.steps["test"].containerImage.isNilOrEmpty
sameContents(pc.steps["test"].artifacts, @[]) sameContents(pc.steps["test"].artifacts, @[])
sameContents(pc.steps["test"].depends, @[]) sameContents(pc.steps["test"].depends, @[])
sameContents(pc.steps["test"].expectedEnv, @[]) sameContents(pc.steps["test"].expectedEnv, @[])