#load "plan.fsx" open System open System.Formats.Tar open System.IO open System.IO.Compression open System.Security.Cryptography open System.Text.Json open System.Text.Json.Nodes open Contract open Plan let revision = Environment.GetEnvironmentVariable "SOURCE_REVISION" let plan = read () let source = "/source/repository/services/project-delivery/controller" let output = "/output" let layout = Path.Combine(output, "layout") let baseImage = "/toolchain/image-base" let go = "/toolchain/usr/lib/go/bin/go" require (validRevision revision && plan.SourceRevision = revision) "source revision mismatch" require (File.Exists go && File.Exists "/toolchain/.ready") "toolchain unavailable" require (Directory.Exists source) "controller source unavailable" require (File.Exists(Path.Combine(baseImage, "index.json"))) "base image unavailable" let executableMode = UnixFileMode.UserRead ||| UnixFileMode.UserWrite ||| UnixFileMode.UserExecute ||| UnixFileMode.GroupRead ||| UnixFileMode.GroupExecute ||| UnixFileMode.OtherRead ||| UnixFileMode.OtherExecute let environment (architecture: string) = [ "CGO_ENABLED", "0" "GOCACHE", "/build-cache" "GOENV", "off" "GOOS", "linux" "GOARCH", architecture "GOMODCACHE", "/toolchain/modules" "GOPATH", "/toolchain/gopath" "GOPROXY", "off" "GOROOT", "/toolchain/usr/lib/go" "HOME", "/tmp" ] let build (architecture: string) = let binary = Path.Combine(output, "bin", architecture, "project-delivery-controller") directory (Path.GetDirectoryName binary) |> ignore requireSuccess go [ "build" "-mod=readonly" "-trimpath" "-ldflags=-s -w -buildid=" "-o" binary "." ] (Some source) (environment architecture) |> ignore File.SetUnixFileMode(binary, executableMode) let header = File.ReadAllBytes binary |> Array.truncate 20 let machine = if architecture = "amd64" then 62us else 183us require (header.Length = 20 && header[0..3] = [| 0x7fuy; 0x45uy; 0x4cuy; 0x46uy |] && header[4..5] = [| 2uy; 1uy |] && BitConverter.ToUInt16(header, 18) = machine) $"invalid {architecture} executable" binary let copyTree (sourceRoot: string) (targetRoot: string) = if Directory.Exists targetRoot then Directory.Delete(targetRoot, true) directory targetRoot |> ignore Directory.EnumerateDirectories(sourceRoot, "*", SearchOption.AllDirectories) |> Seq.iter (fun path -> directory (Path.Combine(targetRoot, Path.GetRelativePath(sourceRoot, path))) |> ignore) Directory.EnumerateFiles(sourceRoot, "*", SearchOption.AllDirectories) |> Seq.iter (fun path -> File.Copy(path, Path.Combine(targetRoot, Path.GetRelativePath(sourceRoot, path)))) let node (path: string) = let value = JsonNode.Parse(File.ReadAllText path) require (not <| isNull value) $"invalid JSON: {path}" value let text (value: JsonNode) = value.GetValue() let digestPath (root: string) (digest: string) = require (validDigest digest) $"invalid OCI digest: {digest}" Path.Combine(root, digest[7..]) let blob (bytes: byte array) = let digest = SHA256.HashData bytes |> Convert.ToHexString |> _.ToLowerInvariant() File.WriteAllBytes(Path.Combine(layout, "blobs/sha256", digest), bytes) $"sha256:{digest}", int64 bytes.Length let createLayer (architecture: string) (binary: string) = let tarPath = Path.Combine(output, $"{architecture}.tar") let gzipPath = tarPath + ".gz" use tarOutput = File.Create tarPath use writer = new TarWriter(tarOutput, TarEntryFormat.Pax, true) let entry = PaxTarEntry(TarEntryType.RegularFile, "project-delivery-controller") entry.Gid <- 0 entry.Uid <- 0 entry.Mode <- executableMode entry.ModificationTime <- DateTimeOffset.UnixEpoch entry.DataStream <- File.OpenRead binary writer.WriteEntry entry entry.DataStream.Dispose() writer.Dispose() tarOutput.Dispose() let diff = sha256 tarPath use input = File.OpenRead tarPath use compressed = File.Create gzipPath use encoder = new GZipStream(compressed, CompressionLevel.SmallestSize, true) input.CopyTo encoder encoder.Dispose() compressed.Dispose() let digest = sha256 gzipPath File.Delete tarPath diff, digest, FileInfo(gzipPath).Length, gzipPath let addLayer (architecture: string) (binary: string) = let index = node (Path.Combine(layout, "index.json")) let descriptor = index["manifests"].AsArray() |> Seq.find (fun item -> text (item["platform"]["architecture"]) = architecture) let blobs = Path.Combine(layout, "blobs/sha256") let manifestPath = digestPath blobs (text (descriptor["digest"])) let manifest = node manifestPath let configDescriptor = manifest["config"] let config = node (digestPath blobs (text (configDescriptor["digest"]))) let diff, digest, size, layerPath = createLayer architecture binary File.Move(layerPath, Path.Combine(blobs, digest), true) let layer = JsonObject() layer["mediaType"] <- "application/vnd.oci.image.layer.v1.tar+gzip" layer["digest"] <- $"sha256:{digest}" layer["size"] <- size manifest["layers"].AsArray().Add(layer) let diffId = $"sha256:{diff}" let rootfs = config["rootfs"] let diffIds = rootfs["diff_ids"].AsArray() diffIds.Add(diffId) let configDigest, configSize = JsonSerializer.SerializeToUtf8Bytes(config, json) |> blob configDescriptor["digest"] <- configDigest configDescriptor["size"] <- configSize let manifestDigest, manifestSize = JsonSerializer.SerializeToUtf8Bytes(manifest, json) |> blob descriptor["digest"] <- manifestDigest descriptor["size"] <- manifestSize File.WriteAllBytes(Path.Combine(layout, "index.json"), JsonSerializer.SerializeToUtf8Bytes(index, json)) let nameIndex () = let indexPath = Path.Combine(layout, "index.json") let index = node indexPath let nested = JsonObject() nested["schemaVersion"] <- 2 nested["mediaType"] <- "application/vnd.oci.image.index.v1+json" nested["manifests"] <- index["manifests"].DeepClone() let digest, size = JsonSerializer.SerializeToUtf8Bytes(nested, json) |> blob let descriptor = JsonObject() descriptor["mediaType"] <- "application/vnd.oci.image.index.v1+json" descriptor["digest"] <- digest descriptor["size"] <- size let annotations = JsonObject() annotations["org.opencontainers.image.ref.name"] <- "delivery" descriptor["annotations"] <- annotations index["manifests"] <- JsonArray(descriptor) File.WriteAllBytes(indexPath, JsonSerializer.SerializeToUtf8Bytes(index, json)) File.WriteAllText(Path.Combine(output, "image-digest"), digest + "\n") digest copyTree baseImage layout [ "amd64"; "arm64" ] |> List.iter (fun architecture -> build architecture |> addLayer architecture) printfn "build=image digest=%s" (nameIndex ())