#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 [] type CacheObject = { [] Key: string [] Sha256: string } [] type Assignment = { [] Origin: string [] Cache: CacheObject [] ExportKey: string [] CacheVersion: string [] InputHashes: Dictionary } [] type InputPlan = { [] Version: int [] Project: string [] SourceRevision: string [] Toolchain: Assignment [] 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() [ "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) = { Origin = "primary" Cache = Unchecked.defaultof ExportKey = $"projects/controller/controller-delivery/cache/{name}/_latest.json" CacheVersion = cacheVersion name InputHashes = Dictionary(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 require (plan.Version = 1 && plan.Project = "controller" && validRevision plan.SourceRevision) "invalid cache plan identity" validateAssignment "toolchain" plan.Toolchain validateAssignment "go-build" plan.Build plan