quorate/services/project-delivery/k8s/workflows/controller/build.fsx

199 lines
6.9 KiB
FSharp

#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<string>()
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 ())