1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
namespace SomhairlesDream.Server
open System
open System.IO
open System.Threading.Tasks
open SomhairlesDream.Modeling
open SomhairlesDream.Shared
type RunStartOutcome =
| Accepted of RunSnapshot
| Conflict of RunSnapshot
type ArtifactLookupError =
| RunNotFound
| RunNotComplete of RunSnapshot
| InvalidArtifactPath of string
| ArtifactNotFound
| InvalidManifest of string
type ArtifactRunCoordinator(
artifactRoot: string,
bridgeFactory: unit -> IArtifactBridge,
clock: unit -> DateTimeOffset,
staleAfter: TimeSpan
) =
let root = Path.GetFullPath(artifactRoot)
let registry = ArtifactRunRegistry(staleAfter, clock)
let validateIds (ids: RunIds) =
[| ids.ProjectId; ids.RunId; ids.BaseId; ids.TargetId |]
|> Array.tryFind (Ids.validateResourceId >> Result.isError)
|> function
| Some invalid -> Error $"invalid resource id: {invalid}"
| None -> Ok ()
let runDirectory (projectId: string) (runId: string) =
Path.Combine(root, projectId, runId)
let failIfNeeded (store: ArtifactRunStore) (ids: RunIds) message =
let snapshot = store.Snapshot()
if snapshot.Status = "idle" || snapshot.Status = "running" then
store.Apply(
RunEvent.Fail
{ Ids = ids
At = clock ()
StepId = snapshot.CurrentStepId
Message = message }
)
|> ignore
let runPipeline (store: ArtifactRunStore) (ids: RunIds) render =
try
let options : PipelineOptions =
{ ArtifactRoot = root
Ids = ids
Render = render
Bridge = bridgeFactory ()
Clock = clock
OnEvent =
fun event ->
match store.Apply event with
| Ok _ -> ()
| Error message -> invalidOp message }
match Pipeline.run options with
| Ok _ -> ()
| Error message -> failIfNeeded store ids message
with ex ->
failIfNeeded store ids ex.Message
let safeArtifactPath (runDirectory: string) (relativePath: string) =
if String.IsNullOrWhiteSpace(relativePath) || Path.IsPathRooted(relativePath) then
Error "artifact path must be relative"
else
let normalized = relativePath.Replace('\\', '/')
let segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries)
if segments |> Array.exists (fun segment -> segment = ".." || segment = ".") then
Error "artifact path contains traversal"
else
let fullPath = Path.GetFullPath(Path.Combine(runDirectory, normalized.Replace('/', Path.DirectorySeparatorChar)))
let prefix = Path.GetFullPath(runDirectory).TrimEnd(Path.DirectorySeparatorChar) + string Path.DirectorySeparatorChar
if fullPath.StartsWith(prefix, StringComparison.Ordinal) then
Ok fullPath
else
Error "artifact path escapes run directory"
member _.ArtifactRoot = root
member _.Start(ids: RunIds, render: bool) : Result<RunStartOutcome, string> =
match validateIds ids with
| Error message -> Error message
| Ok () ->
match registry.TryCreate(ids) with
| Some store ->
Task.Run(fun () -> runPipeline store ids render) |> ignore
Ok(Accepted(store.Snapshot()))
| None ->
match registry.TryFind(ids.ProjectId, ids.RunId) with
| Some store -> Ok(Conflict(store.Observe(clock ())))
| None -> Error "run registry entry disappeared"
member _.TryFind(projectId: string, runId: string) = registry.TryFind(projectId, runId)
member _.Subscribe(projectId: string, runId: string) =
registry.TryFind(projectId, runId) |> Option.map (fun store -> store.Subscribe())
member _.ObserveAll() = registry.ObserveAll(clock ())
member _.Manifest(projectId: string, runId: string) : Result<ArtifactManifest, ArtifactLookupError> =
match registry.TryFind(projectId, runId) with
| None -> Error RunNotFound
| Some store ->
let snapshot = store.Observe(clock ())
if snapshot.Status <> "complete" then
Error(RunNotComplete snapshot)
else
let path = Path.Combine(runDirectory projectId runId, "manifest.json")
match ArtifactVerifier.verify path with
| Ok manifest -> Ok manifest
| Error message -> Error(InvalidManifest message)
member _.Artifact(projectId: string, runId: string, relativePath: string) : Result<string, ArtifactLookupError> =
match registry.TryFind(projectId, runId) with
| None -> Error RunNotFound
| Some store ->
let snapshot = store.Observe(clock ())
if snapshot.Status <> "complete" then
Error(RunNotComplete snapshot)
else
let directory = runDirectory projectId runId
match safeArtifactPath directory relativePath with
| Error message -> Error(InvalidArtifactPath message)
| Ok path when not (File.Exists(path)) -> Error ArtifactNotFound
| Ok path -> Ok path
member _.StepArtifact(projectId: string, runId: string, relativePath: string) : Result<string, ArtifactLookupError> =
match registry.TryFind(projectId, runId) with
| None -> Error RunNotFound
| Some _ ->
let directory = runDirectory projectId runId
match safeArtifactPath directory relativePath with
| Error message -> Error(InvalidArtifactPath message)
| Ok path when not (path.EndsWith(".glb", StringComparison.OrdinalIgnoreCase)) ->
Error(InvalidArtifactPath "step artifacts must be .glb files")
| Ok path when not (path.StartsWith(Path.Combine(directory, "steps") + string Path.DirectorySeparatorChar, StringComparison.Ordinal)) ->
Error(InvalidArtifactPath "step artifacts must live under steps/")
| Ok path when not (File.Exists(path)) -> Error ArtifactNotFound
| Ok path -> Ok path
|