199 lines
6.9 KiB
FSharp
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 ())
|