157 lines
5.2 KiB
Text
157 lines
5.2 KiB
Text
|
|
#load "contract.fsx"
|
||
|
|
|
||
|
|
open System
|
||
|
|
open System.Collections.Generic
|
||
|
|
open System.Diagnostics
|
||
|
|
open System.IO
|
||
|
|
open System.Text.Json
|
||
|
|
open System.Text.Json.Serialization
|
||
|
|
open Contract
|
||
|
|
|
||
|
|
[<CLIMutable>]
|
||
|
|
type CacheObject =
|
||
|
|
{ [<JsonPropertyName("key")>]
|
||
|
|
Key: string
|
||
|
|
[<JsonPropertyName("sha256")>]
|
||
|
|
Sha256: string }
|
||
|
|
|
||
|
|
[<CLIMutable>]
|
||
|
|
type Assignment =
|
||
|
|
{ [<JsonPropertyName("origin")>]
|
||
|
|
Origin: string
|
||
|
|
[<JsonPropertyName("cache")>]
|
||
|
|
Cache: CacheObject
|
||
|
|
[<JsonPropertyName("exportKey")>]
|
||
|
|
ExportKey: string
|
||
|
|
[<JsonPropertyName("cacheVersion")>]
|
||
|
|
CacheVersion: string
|
||
|
|
[<JsonPropertyName("inputHashes")>]
|
||
|
|
InputHashes: Dictionary<string, string> }
|
||
|
|
|
||
|
|
[<CLIMutable>]
|
||
|
|
type InputPlan =
|
||
|
|
{ [<JsonPropertyName("version")>]
|
||
|
|
Version: int
|
||
|
|
[<JsonPropertyName("project")>]
|
||
|
|
Project: string
|
||
|
|
[<JsonPropertyName("sourceRevision")>]
|
||
|
|
SourceRevision: string
|
||
|
|
[<JsonPropertyName("toolchain")>]
|
||
|
|
Toolchain: Assignment
|
||
|
|
[<JsonPropertyName("build")>]
|
||
|
|
Build: Assignment }
|
||
|
|
|
||
|
|
let private planPath = "/state/cache-plan.json"
|
||
|
|
let private sourceRoot = "/source/repository"
|
||
|
|
|
||
|
|
let private controllerRoot =
|
||
|
|
Path.Combine(sourceRoot, "services/project-delivery/controller")
|
||
|
|
|
||
|
|
let private identity fields =
|
||
|
|
fields |> String.concat "\n" |> (fun value -> hashText (value + "\n"))
|
||
|
|
|
||
|
|
let private inputHashes () =
|
||
|
|
let hashes = Dictionary<string, string>()
|
||
|
|
|
||
|
|
[ "services/project-delivery/controller/go.mod"
|
||
|
|
"services/project-delivery/controller/go.sum" ]
|
||
|
|
|> List.iter (fun path ->
|
||
|
|
let source = Path.Combine(sourceRoot, path)
|
||
|
|
require (File.Exists source) $"missing cache input: {path}"
|
||
|
|
hashes[path] <- sha256 source)
|
||
|
|
|
||
|
|
hashes
|
||
|
|
|
||
|
|
let private cacheVersion name =
|
||
|
|
let files =
|
||
|
|
match name with
|
||
|
|
| "toolchain" ->
|
||
|
|
[ "cache.fsx"
|
||
|
|
"contract.fsx"
|
||
|
|
"controller.apko.yaml"
|
||
|
|
"plan.fsx"
|
||
|
|
"toolchain.fsx" ]
|
||
|
|
| "go-build" -> [ "build.fsx"; "cache.fsx"; "contract.fsx"; "plan.fsx"; "toolchain.fsx" ]
|
||
|
|
| _ -> fail "invalid cache name"
|
||
|
|
|
||
|
|
[ "controller-delivery-cache-v1"
|
||
|
|
name
|
||
|
|
"apko=1.2.39-r0"
|
||
|
|
"go-1.27=1.27.3-r0"
|
||
|
|
"ca-certificates-bundle=20261020-r0" ]
|
||
|
|
@ (files
|
||
|
|
|> List.map (fun path ->
|
||
|
|
let digest = sha256 (Path.Combine("/orchestration", path))
|
||
|
|
$"{path}={digest}"))
|
||
|
|
|> identity
|
||
|
|
|
||
|
|
let private assignment (name: string) (hashes: Dictionary<string, string>) =
|
||
|
|
{ Origin = "primary"
|
||
|
|
Cache = Unchecked.defaultof<CacheObject>
|
||
|
|
ExportKey = $"projects/controller/controller-delivery/cache/{name}/_latest.json"
|
||
|
|
CacheVersion = cacheVersion name
|
||
|
|
InputHashes = Dictionary<string, string>(hashes) }
|
||
|
|
|
||
|
|
let create (revision: string) (select: string -> Assignment -> Assignment) =
|
||
|
|
require (validRevision revision) "invalid source revision"
|
||
|
|
require (Directory.Exists controllerRoot) "controller source is unavailable"
|
||
|
|
let hashes = inputHashes ()
|
||
|
|
|
||
|
|
{ Version = 1
|
||
|
|
Project = "controller"
|
||
|
|
SourceRevision = revision
|
||
|
|
Toolchain = select "toolchain" (assignment "toolchain" hashes)
|
||
|
|
Build = select "go-build" (assignment "go-build" hashes) }
|
||
|
|
|
||
|
|
let write (plan: InputPlan) =
|
||
|
|
directory (Path.GetDirectoryName planPath) |> ignore
|
||
|
|
let temporary = planPath + ".incoming"
|
||
|
|
File.WriteAllText(temporary, JsonSerializer.Serialize(plan, json))
|
||
|
|
File.Move(temporary, planPath, true)
|
||
|
|
|
||
|
|
let private validateAssignment (name: string) (assignment: Assignment) =
|
||
|
|
require (not <| isNull (box assignment)) $"missing {name} assignment"
|
||
|
|
|
||
|
|
require
|
||
|
|
(assignment.ExportKey = $"projects/controller/controller-delivery/cache/{name}/_latest.json")
|
||
|
|
$"invalid {name} cache key"
|
||
|
|
|
||
|
|
require (validSha256 assignment.CacheVersion) $"invalid {name} cache version"
|
||
|
|
require (not <| isNull (box assignment.InputHashes)) $"missing {name} input hashes"
|
||
|
|
|
||
|
|
assignment.InputHashes
|
||
|
|
|> Seq.iter (fun item ->
|
||
|
|
require (not (String.IsNullOrWhiteSpace item.Key) && validSha256 item.Value) $"invalid {name} input hash")
|
||
|
|
|
||
|
|
match assignment.Origin with
|
||
|
|
| "primary" -> require (isNull (box assignment.Cache)) $"unexpected {name} cache object"
|
||
|
|
| "cache" ->
|
||
|
|
require
|
||
|
|
(not <| isNull (box assignment.Cache)
|
||
|
|
&& assignment.Cache.Key.StartsWith(
|
||
|
|
$"projects/controller/controller-delivery/cache/{name}/objects/",
|
||
|
|
StringComparison.Ordinal
|
||
|
|
)
|
||
|
|
&& assignment.Cache.Key.EndsWith(".tar.zst", StringComparison.Ordinal)
|
||
|
|
&& validSha256 assignment.Cache.Sha256)
|
||
|
|
$"invalid {name} cache object"
|
||
|
|
| _ -> fail $"invalid {name} cache origin"
|
||
|
|
|
||
|
|
let read () =
|
||
|
|
let elapsed = Stopwatch.StartNew()
|
||
|
|
|
||
|
|
while not (File.Exists planPath) && elapsed.Elapsed < TimeSpan.FromMinutes 15.0 do
|
||
|
|
sleep 100
|
||
|
|
|
||
|
|
require (File.Exists planPath) "cache plan unavailable"
|
||
|
|
let plan = File.ReadAllText(planPath) |> decode<InputPlan>
|
||
|
|
|
||
|
|
require
|
||
|
|
(plan.Version = 1
|
||
|
|
&& plan.Project = "controller"
|
||
|
|
&& validRevision plan.SourceRevision)
|
||
|
|
"invalid cache plan identity"
|
||
|
|
|
||
|
|
validateAssignment "toolchain" plan.Toolchain
|
||
|
|
validateAssignment "go-build" plan.Build
|
||
|
|
plan
|