package oci import ( "context" "crypto/sha256" "fmt" "os" "path/filepath" "runtime" "strconv" "syscall" "time" "archive/tar" "compress/gzip" "encoding/hex" "encoding/json" "io" "io/fs" "os/exec" slashpath "path" "strings" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/pkg/errors" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/scaffolding" ) const ( DefaultUid = 1000 DefaultGid = 1000 ) var defaultIgnored = []string{ ".git", ".func", ".funcignore", ".gitignore", } var builders = map[string]languageBuilder{ "go": goBuilder{}, "python": pythonBuilder{}, } // IsSupported is for UX. func IsSupported(runtime string) bool { _, ok := builders[runtime] return ok } type imageLayer struct { Descriptor v1.Descriptor Layer v1.Layer } type languageBuilder interface { // Base returns the base image (if any) to use. Ideally this is a // multi-arch base image with a corresponding platform image for // each requested to be built. Base() string // WriteShared layers (not platform-specific) which need to be genearted // on demand per language, such as shared dependencies. WriteShared(buildJob) ([]imageLayer, error) // WritePlatform layers which are specific to the WritePlatform(buildJob, v1.Platform) ([]imageLayer, error) // Configure a config with, for example, the entrypoint. // Called once per platform. Configure(buildJob, v1.Platform, v1.ConfigFile) (v1.ConfigFile, error) } type Builder struct { name string // TODO: why is this used again? verbose bool // log verbosely onDone func() // For testing, an on done notification impl languageBuilder // For testing, an override for build impl } // NewBuilder creates a builder instance. func NewBuilder(name string, verbose bool) *Builder { return &Builder{name: name, verbose: verbose, onDone: func() {}} } // Build an OCI image of the given Function, wrapped in a service which // exposes the function as a network service. // // Platforms are optional and default to fn.DefaultPlatforms. func (b *Builder) Build(ctx context.Context, f fn.Function, pp []fn.Platform) (err error) { if len(pp) == 0 { pp = fn.DefaultPlatforms // Use Default platforms if not provided } job, err := newBuildJob(ctx, f, pp, b.verbose) // Create a new build job if err != nil { return } if b.impl != nil { job.languageBuilder = b.impl // override builder if requested (tests) } if err = setup(job); err != nil { // create some directories etc return } defer cleanup(job) if err = scaffold(job); err != nil { // write out the service wrapper return } if err = containerize(job); err != nil { // write image to .func/builds return } if err = updateLastLink(job); err != nil { // .func/builds/last return } b.onDone() // signal optional async done event listener (tests) return // TODO: communicating build completeness throgh returning without error // is suboptimal. The system then relies on the implicit availability // of the OCI image in this process' build directory // // It Would be better to have a defined build result object returned here // which can be used to communicate details of the build to the pusher // explicitly, such as where the image to be pushed can be found, rather // than this implicit coupling. } // setup the build task prerequsites on the filesystem func setup(job buildJob) (err error) { // Fail if another build is in progress if job.isActive() { return ErrBuildInProgress{job.buildDir()} } // Build directory if _, err = os.Stat(job.buildDir()); !os.IsNotExist(err) { if job.verbose { fmt.Fprintf(os.Stderr, "rm -rf %v\n", job.buildDir()) } if err = os.RemoveAll(job.buildDir()); err != nil { return } } if job.verbose { fmt.Fprintf(os.Stderr, "mkdir -p %v\n", job.buildDir()) } if err = os.MkdirAll(job.buildDir(), 0774); err != nil { return } // PID links directory if _, err = os.Stat(job.pidsDir()); os.IsNotExist(err) { if job.verbose { fmt.Fprintf(os.Stderr, "mkdir -p %v\n", job.pidsDir()) } if err = os.MkdirAll(job.pidsDir(), 0774); err != nil { return } } // Link to last build attempted (this) target := filepath.Join("..", "by-hash", job.hash) if job.verbose { fmt.Fprintf(os.Stderr, "ln -s %v %v\n", target, job.pidLink()) } if err = os.Symlink(target, job.pidLink()); err != nil { return err } // Creates the blobs directory where layer data resides // (compressed and hashed) if err := os.MkdirAll(job.blobsDir(), os.ModePerm); err != nil { return err } // Blob cache directory for shared base layers between builds. // NOTE: may turn this into a system-global cache (if available) at // XDG_CONFIG_HOME/func/image-cache, with this as a fallback: // TODO: it's possible, though unlikely, this directory could // grow unweildy under active development after rounds of changes to // the used base layers. We should have some way to truncate or otherwise // mitigate this disk memory leak potential. if err := os.MkdirAll(job.cacheDir(), os.ModePerm); err != nil { return err } return } // cleanup various filesystem artifacts of the build. func cleanup(job buildJob) { // cleanup orphaned build links dd, _ := os.ReadDir(job.pidsDir()) for _, d := range dd { if processExists(d.Name()) { continue } dir := filepath.Join(job.pidsDir(), d.Name()) if job.verbose { fmt.Fprintf(os.Stderr, "rm %v\n", dir) } _ = os.RemoveAll(dir) } // remove build file directories unless they are either: // 1. The build files from the last successful build // 2. Are associated with a pid link (currently in progress) dd, _ = os.ReadDir(job.buildsDir()) for _, d := range dd { dir := filepath.Join(job.buildsDir(), d.Name()) if isLinkTo(job.lastLink(), dir) { continue } if job.isActive() { continue } if job.verbose { fmt.Fprintf(os.Stderr, "rm %v\n", dir) } _ = os.RemoveAll(dir) } } // scaffold writes out the process wrapper code which will instantiate the // Function and expose it as a service when included in the final container. func scaffold(job buildJob) (err error) { // extract the embedded filesystem which holds the scaffolding for // the given runtime repo, err := fn.NewRepository("", "") if err != nil { return } return scaffolding.Write( job.buildDir(), // desintation for scaffolding job.function.Root, // source to be scaffolded job.function.Runtime, // scaffolding language to write job.function.Invoke, repo.FS()) } // containerize the full service which consists of the scaffolded Function, // Function implementation, base image, data layers etc. // This container is stored on disk for later upload to the registry via // the configured pusher. func containerize(job buildJob) error { sharedLayers := []imageLayer{} // Write out the static, required oci-layout file if err := os.WriteFile(filepath.Join(job.ociDir(), "oci-layout"), []byte(`{ "imageLayoutVersion": "1.0.0" }`), os.ModePerm); err != nil { return err } // Create the shared data layer, returning its metadata data, err := writeDataLayer(job) // shared if err != nil { return err } sharedLayers = append(sharedLayers, data) // Create the shared root certificates layer, returning its metadata certs, err := writeCertsLayer(job) // shared if err != nil { return err } sharedLayers = append(sharedLayers, certs) // Create any shared layers from the language-specific builder, returning // their metadata. shared, err := job.languageBuilder.WriteShared(job) if err != nil { return err } sharedLayers = append(sharedLayers, shared...) // Manifests are the metadata for individual platform-specific images // are used to create the final multi-arch image. manifests := []v1.Descriptor{} // For each platform, create a new slice of layers consisting of first // the shared layers followed by newly-generated platform-specific layers // for each platform. Bundle these into a new image manifest for inclusion // in the final container. for _, p := range job.platforms { // Write out the platform-specific layers, returning their metadata. platformSpecificLayers, err := job.languageBuilder.WritePlatform(job, p) if err != nil { return err } layers := append(sharedLayers, platformSpecificLayers...) // Fetch the base image if specified. // base layers are added to blobs (and cached) base, err := pullBase(job, p) if err != nil { return err } // Create a config file which describes this platform image. configFile, err := newConfigFile(job, p, base, layers) if err != nil { return err } // Allow the languageBuilder to do any additional config such as // setting the platform image's command, adding environment vars etc. configFile, err = job.languageBuilder.Configure(job, p, configFile) if err != nil { return err } // Write out the config for the platform image, returning its metadata config, err := writeConfig(job, configFile) if err != nil { return err } // Create the image, returning its descriptor for inclusin in the // final container config. manifest, err := writeManifest(job, p, base, config, layers) if err != nil { return err } manifests = append(manifests, manifest) } // Image Index which enumerates all images via manifests return writeIndex(job, manifests) } // writeDataLayer creates the shared data layer in the container file hierarchy and // returns both its descriptor and layer metadata. func writeDataLayer(job buildJob) (layer imageLayer, err error) { // Create the data tarball // TODO: try WithCompressedCaching? source := job.function.Root // The source is the function's entire filesystem target := filepath.Join(job.buildDir(), "datalayer.tar.gz") if err = newDataTarball(source, target, defaultIgnored, job.verbose); err != nil { return } // Layer if layer.Layer, err = tarball.LayerFromFile(target); err != nil { return } // Descriptor if layer.Descriptor, err = newDescriptor(layer.Layer); err != nil { return } // Blob blob := filepath.Join(job.blobsDir(), layer.Descriptor.Digest.Hex) if job.verbose { fmt.Fprintf(os.Stderr, "mv %v %v\n", rel(job.buildDir(), target), rel(job.buildDir(), blob)) } err = os.Rename(target, blob) return } func newDataTarball(root, target string, ignored []string, verbose bool) error { targetFile, err := os.Create(target) if err != nil { return err } defer targetFile.Close() gw := gzip.NewWriter(targetFile) defer gw.Close() tw := tar.NewWriter(gw) defer tw.Close() return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // Skip files explicitly ignored for _, v := range ignored { if info.Name() == v { if info.IsDir() { return filepath.SkipDir } return nil } } lnk := "" // if link, this will be used as the target if info.Mode()&fs.ModeSymlink != 0 { if lnk, err = validatedLinkTarget(root, path); err != nil { return err } } header, err := tar.FileInfoHeader(info, lnk) if err != nil { return err } relPath, err := filepath.Rel(root, path) if err != nil { return err } header.Name = slashpath.Join("/func", filepath.ToSlash(relPath)) header.Uid = DefaultUid header.Gid = DefaultGid if err := tw.WriteHeader(header); err != nil { return err } if verbose { fmt.Fprintf(os.Stderr, "→ %v \n", header.Name) } if !info.Mode().IsRegular() { //nothing more to do for non-regular return nil } file, err := os.Open(path) if err != nil { return err } defer file.Close() _, err = io.Copy(tw, file) return err }) } // validatedLinkTarget returns the target of a given link or an error if // that target is either absolute or outside the given project root. func validatedLinkTarget(root, path string) (tgt string, err error) { // tgt is the raw target of the link. // This path is either absolute or relative to the link's location. tgt, err = os.Readlink(path) if err != nil { return tgt, fmt.Errorf("cannot read link: %w", err) } // Absolute links will not be correct when copied into the runtime // container, because they are placed into path into '/func', if filepath.IsAbs(tgt) { return tgt, errors.New("project may not contain absolute links") } // Calculate the actual target of the link // (relative to the parent of the symlink) lnkTgt := filepath.Join(filepath.Dir(path), tgt) // Calculate the relative path from the function's root to // this actual target location relLnkTgt, err := filepath.Rel(root, lnkTgt) if err != nil { return } // Fail if this path is outside the function's root. if strings.HasPrefix(relLnkTgt, ".."+string(filepath.Separator)) || relLnkTgt == ".." { return tgt, errors.New("links must stay within project root") } return } // newCertLayer creates the shared data layer in the container file hierarchy and // returns both its descriptor and layer metadata. func writeCertsLayer(job buildJob) (layer imageLayer, err error) { // Create the data tarball // TODO: try WithCompressedCaching? source := filepath.Join(job.buildDir(), "ca-certificates.crt") target := filepath.Join(job.buildDir(), "certslayer.tar.gz") if err = newCertsTarball(source, target, job.verbose); err != nil { return } // Layer if layer.Layer, err = tarball.LayerFromFile(target); err != nil { return } // Descriptor if layer.Descriptor, err = newDescriptor(layer.Layer); err != nil { return } // Blob blob := filepath.Join(job.blobsDir(), layer.Descriptor.Digest.Hex) if job.verbose { fmt.Fprintf(os.Stderr, "mv %v %v\n", rel(job.buildDir(), target), rel(job.buildDir(), blob)) } err = os.Rename(target, blob) return } func newCertsTarball(source, target string, verbose bool) error { targetFile, err := os.Create(target) if err != nil { return err } defer targetFile.Close() gw := gzip.NewWriter(targetFile) defer gw.Close() tw := tar.NewWriter(gw) defer tw.Close() paths := []string{ "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-certificates.crt", } fi, err := os.Stat(source) if err != nil { return err } // For each ssl certs path we want to create for _, path := range paths { // Create a header for it header, err := tar.FileInfoHeader(fi, "") if err != nil { return err } header.Name = path header.Uid = DefaultUid header.Gid = DefaultGid if err := tw.WriteHeader(header); err != nil { return err } if verbose { fmt.Fprintf(os.Stderr, "→ %v \n", header.Name) } file, err := os.Open(source) if err != nil { return err } defer file.Close() _, err = io.Copy(tw, file) if err != nil { return err } } return nil } // pullBase image returns the descriptor to a remote image for the given // platform if a base image was specified for this builder. // Its layers are automatically downloaded into the local cache if this is // the first fetch and their blobs linked into the final OCI image. func pullBase(job buildJob, p v1.Platform) (image v1.Image, err error) { if job.languageBuilder.Base() == "" { return // FROM SCRATCH } // Parse the base into a reference ref, err := name.ParseReference(job.languageBuilder.Base()) if err != nil { return } // Get the remote descriptor referenced desc, err := remote.Get(ref, remote.WithPlatform(p)) if err != nil { return } // Get the image described, either directly or via platform dereference // from an index: if image, err = desc.Image(); err != nil { return } // Write the image's layer data into the OCI blobs (caching) layers, err := image.Layers() if err != nil { return } for _, layer := range layers { if err = writeBaseLayer(job, layer); err != nil { return } } return } func writeBaseLayer(job buildJob, layer v1.Layer) (err error) { if err = ensureCached(job, layer); err != nil { return } digest, err := layer.Digest() if err != nil { return } sourcePath := filepath.Join(job.cacheDir(), digest.Hex) destPath := filepath.Join(job.blobsDir(), digest.Hex) // Check if already added if _, err := os.Stat(destPath); !os.IsNotExist(err) { return nil // layer already in blobs. } // Add it to the image via hard link if err := os.Link(sourcePath, destPath); err != nil { return fmt.Errorf("creating hard link for layer %s: %w", digest, err) } return // TODO: fallback to copying eg if windows without perms? } func ensureCached(job buildJob, layer v1.Layer) (err error) { digest, err := layer.Digest() if err != nil { return } cachePath := filepath.Join(job.cacheDir(), digest.Hex) if _, err = os.Stat(cachePath); !os.IsNotExist(err) { if job.verbose { fmt.Fprintf(os.Stderr, "Using cached base layer: %v\n", digest.Hex) } return } reader, err := layer.Compressed() if err != nil { return } defer reader.Close() file, err := os.Create(cachePath) if err != nil { return } if _, err = io.Copy(file, reader); err != nil { return } if job.verbose { fmt.Fprintf(os.Stderr, "Caching base image layer: %v\n", digest.Hex) } return } func newConfigFile(job buildJob, p v1.Platform, base v1.Image, imageLayers []imageLayer) (cfg v1.ConfigFile, err error) { cfg = v1.ConfigFile{ Created: v1.Time{Time: job.start}, Architecture: p.Architecture, OS: p.OS, OSVersion: p.OSVersion, Variant: p.Variant, // OSFeatures: p.OSFeatures, // TODO: need to update dep to get this Config: v1.Config{ Env: newConfigEnvs(job), Volumes: newConfigVolumes(job), ExposedPorts: map[string]struct{}{"8080/tcp": {}}, WorkingDir: "/func/", StopSignal: "SIGKILL", User: fmt.Sprintf("%v:%v", DefaultUid, DefaultGid), // Labels }, // TODO: Create a separate history entry for each layer built for // each language (EmptyLayer=false). History: []v1.History{ { Author: "func", Created: v1.Time{Time: job.start}, Comment: "func host builder", EmptyLayer: true, }, }, RootFS: v1.RootFS{ Type: "layers", DiffIDs: []v1.Hash{}, }, } // Populate Layer DiffIDs for _, imageLayer := range imageLayers { diffID, err := imageLayer.Layer.DiffID() if err != nil { return cfg, err } cfg.RootFS.DiffIDs = append(cfg.RootFS.DiffIDs, diffID) } // Base Images // Carry over settings from the base. if base != nil { // Fetch base's config file baseCfg, err := base.ConfigFile() if err != nil { return cfg, err } // Reuse the base's user if defined if baseCfg.Config.User != "" { cfg.Config.User = baseCfg.Config.User } // Prepend ENVs cfg.Config.Env = append(baseCfg.Config.Env, cfg.Config.Env...) // Prepend history cfg.History = append(baseCfg.History, cfg.History...) // Prepend diffIDs cfg.RootFS.DiffIDs = append(baseCfg.RootFS.DiffIDs, cfg.RootFS.DiffIDs...) } return cfg, nil } // newConfigEnvs returns the final set of environment variables to build into // the container. This consists of func-provided build metadata envs as well // as any environment variables provided on the function itself. func newConfigEnvs(job buildJob) []string { envs := []string{} // FUNC_CREATED // Formats container timestamp as RFC3339; a stricter version of the ISO 8601 // format used by the container image manifest's 'Created' attribute. envs = append(envs, "FUNC_CREATED="+job.start.Format(time.RFC3339)) // FUNC_VERSION // If source controlled, and if being built from a system with git, the // environment FUNC_VERSION will be populated. Otherwise it will exist // (to indicate this logic was executed) but have an empty value. if job.verbose { fmt.Fprintf(os.Stderr, "cd %v && export FUNC_VERSION=$(git describe --tags)\n", job.function.Root) } cmd := exec.CommandContext(job.ctx, "git", "describe", "--tags") cmd.Dir = job.function.Root output, err := cmd.Output() if err != nil { if job.verbose { fmt.Fprintf(os.Stderr, "unable to determine function version. %v\n", err) } envs = append(envs, "FUNC_VERSION=") } else { envs = append(envs, "FUNC_VERSION="+strings.TrimSpace(string(output))) } // TODO: OTHERS? // Other metadata that may be useful. Perhaps: // - func client version (func cli) used when building this file? // - user/environment which triggered this build? // - A reflection of the function itself? Image, registry, etc. etc? // ENVs defined on the Function return append(envs, job.function.Run.Envs.Slice()...) } func newConfigVolumes(job buildJob) map[string]struct{} { volumes := make(map[string]struct{}) for _, v := range job.function.Run.Volumes { if v.Path == nil { continue // TODO: remove pointers from Volume and Env struct members } volumes[*v.Path] = struct{}{} } return volumes } func writeConfig(job buildJob, configFile v1.ConfigFile) (configDesc v1.Descriptor, err error) { configDesc, err = writeAsJSONBlob(job, "config.json", configFile) configDesc.MediaType = types.OCIConfigJSON return } // writeManifest creates an image manifest for the given platform. // The image consists of the shared data layer which is provided func writeManifest(job buildJob, p v1.Platform, base v1.Image, configDesc v1.Descriptor, layers []imageLayer) (v1.Descriptor, error) { // the layers for the final manifest. layerDescs := []v1.Descriptor{} // If a base was provided, prepend it's layers. if base != nil { // base is a v1.Image baseManifest, err := base.Manifest() if err != nil { return v1.Descriptor{}, err } layerDescs = baseManifest.Layers } // Append our layers for _, layer := range layers { layerDescs = append(layerDescs, layer.Descriptor) } // The final manifest for this platform's image manifest := v1.Manifest{ SchemaVersion: 2, MediaType: types.OCIManifestSchema1, Config: configDesc, Layers: layerDescs, } // Write it to blobs manifestDesc, err := writeAsJSONBlob( job, fmt.Sprintf("manifest.%v.%v.json", p.OS, p.Architecture), manifest) manifestDesc.MediaType = types.OCIManifestSchema1 manifestDesc.Platform = &p // returning the blob's descriptor for inclusion in the index return manifestDesc, err } func writeIndex(job buildJob, manifests []v1.Descriptor) (err error) { index := v1.IndexManifest{ SchemaVersion: 2, MediaType: types.OCIImageIndex, Manifests: manifests, } filePath := filepath.Join(job.ociDir(), "index.json") file, err := os.Create(filePath) if err != nil { return } defer file.Close() enc := json.NewEncoder(file) enc.SetIndent("", " ") err = enc.Encode(index) return } // ----------------------- // Build Job // ----------------------- // // A struct which gathers configuration together for a single build job // and provides some calculated fields for a little syntactic sugar. // buildJob contains various settings for a single build type buildJob struct { ctx context.Context // build context start time.Time // Timestamp for this build hash string // a fingerprint of the fs at start function fn.Function // Function being built platforms []v1.Platform // Platforms to build languageBuilder languageBuilder // build implementation verbose bool } // newBuildJob creates a struct which contains information about the current // build job and convenience accessors to eg pertinent directories. func newBuildJob(ctx context.Context, f fn.Function, pp []fn.Platform, verbose bool) (buildJob, error) { job := buildJob{ ctx: ctx, start: time.Now(), function: f, platforms: toPlatforms(pp), verbose: verbose, } // Calculate a hash of the Function filesystem at time of start. var err error if job.hash, _, err = fn.Fingerprint(job.function.Root); err != nil { return job, fmt.Errorf("error calculating fingerprint for build. %w", err) } // Get the builder registered for this language var ok bool if job.languageBuilder, ok = builders[f.Runtime]; !ok { return job, fmt.Errorf("%v functions are not yet supported by the host builder", f.Runtime) } return job, nil } // some convenience accessors func (j buildJob) lastLink() string { return filepath.Join(j.function.Root, fn.RunDataDir, "builds", "last") } func (j buildJob) pidsDir() string { return filepath.Join(j.function.Root, fn.RunDataDir, "builds", "by-pid") } func (j buildJob) pidLink() string { return filepath.Join(j.function.Root, fn.RunDataDir, "builds", "by-pid", strconv.Itoa(os.Getpid())) } func (j buildJob) buildsDir() string { return filepath.Join(j.function.Root, fn.RunDataDir, "builds", "by-hash") } func (j buildJob) buildDir() string { return filepath.Join(j.function.Root, fn.RunDataDir, "builds", "by-hash", j.hash) } func (j buildJob) ociDir() string { return filepath.Join(j.function.Root, fn.RunDataDir, "builds", "by-hash", j.hash, "oci") } func (j buildJob) blobsDir() string { return filepath.Join(j.function.Root, fn.RunDataDir, "builds", "by-hash", j.hash, "oci", "blobs", "sha256") } func (j buildJob) cacheDir() string { return filepath.Join(j.function.Root, fn.RunDataDir, "blob-cache") } // isActive returns false if an active build for this Function is detected. func (j buildJob) isActive() bool { dd, _ := os.ReadDir(j.pidsDir()) for _, d := range dd { // for each link in PIDs dir // the build is active if a process exists of the same name // AND it is a link to this job's build directory. link := filepath.Join(j.pidsDir(), d.Name()) if processExists(d.Name()) && isLinkTo(link, j.buildDir()) { return true } } return false } // ------------------------- // Helpers // ------------------------- // rel is a simple prefix trim used exclusively for verbose debugging // statements to print paths as relative to the current build directory // rather than absolute. Returns the path relative to the current working // build directory. If it is not a subpath, the full path is returned // unchanged. func rel(base, path string) string { if strings.HasPrefix(path, base) { return "." + strings.TrimPrefix(path, base) } return path } // processExists returns true if the process with the given PID // exists. func processExists(pid string) bool { p, err := strconv.Atoi(pid) if err != nil { return false } process, err := os.FindProcess(p) if err != nil { return false } if runtime.GOOS == "windows" { return true } err = process.Signal(syscall.Signal(0)) return err == nil } // isLinkTo returns true if link is a link to target. func isLinkTo(link, target string) bool { var err error if link, err = filepath.EvalSymlinks(link); err != nil { return false } if link, err = filepath.Abs(link); err != nil { return false } if target, err = filepath.EvalSymlinks(target); err != nil { return false } if target, err = filepath.Abs(target); err != nil { return false } return link == target } func updateLastLink(job buildJob) error { if job.verbose { fmt.Fprintf(os.Stderr, "ln -s %v %v\n", job.buildDir(), job.lastLink()) } _ = os.RemoveAll(job.lastLink()) rp, err := filepath.Rel(filepath.Dir(job.lastLink()), job.buildDir()) if err != nil { return err } return os.Symlink(rp, job.lastLink()) } // toPlatforms converts func's implementation-agnostic Platform struct // into to the OCI builder's implementation-specific go-containerregistry v1 // palatform. // Examples: // {OS: "linux", Architecture: "amd64"}, // {OS: "linux", Architecture: "arm64"}, // {OS: "linux", Architecture: "arm", Variant: "v6"}, // {OS: "linux", Architecture: "arm", Variant: "v7"}, // {OS: "darwin", Architecture: "amd64"}, // {OS: "darwin", Architecture: "arm64"}, func toPlatforms(pp []fn.Platform) []v1.Platform { platforms := make([]v1.Platform, len(pp)) for i, p := range pp { platforms[i] = v1.Platform{OS: p.OS, Architecture: p.Architecture, Variant: p.Variant} } return platforms } func newDescriptor(layer v1.Layer) (desc v1.Descriptor, err error) { size, err := layer.Size() if err != nil { return } digest, err := layer.Digest() if err != nil { return } return v1.Descriptor{ MediaType: types.OCILayer, Size: size, Digest: digest, }, nil } // writeAsJSONBlob encodes the object a json, creates a blob from it, and returns // a partially-complted descriptor with the hash and size populated. func writeAsJSONBlob(job buildJob, tempName string, data any) (desc v1.Descriptor, err error) { filePath := filepath.Join(job.buildDir(), tempName) file, err := os.Create(filePath) if err != nil { return } defer file.Close() h := sha256.New() w := io.MultiWriter(file, h) enc := json.NewEncoder(w) enc.SetIndent("", " ") if err = enc.Encode(data); err != nil { return } hash := v1.Hash{Algorithm: "sha256", Hex: hex.EncodeToString(h.Sum(nil))} fileInfo, err := file.Stat() if err != nil { return } size := fileInfo.Size() // move -> blobs blobPath := filepath.Join(job.blobsDir(), hash.Hex) if job.verbose { fmt.Fprintf(os.Stderr, "mv %v %v\n", rel(job.buildDir(), filePath), rel(job.buildDir(), blobPath)) } // Need to close before rename if err = file.Close(); err != nil { return } if err = os.Rename(filePath, blobPath); err != nil { return } return v1.Descriptor{ Digest: hash, Size: size, }, nil }