mirror of https://github.com/knative/func.git
307 lines
7.8 KiB
Go
307 lines
7.8 KiB
Go
package oci
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
|
|
fn "knative.dev/func/pkg/functions"
|
|
"knative.dev/func/pkg/scaffolding"
|
|
)
|
|
|
|
var path = filepath.Join
|
|
|
|
var defaultIgnored = []string{ // TODO: implement and use .funcignore
|
|
".git",
|
|
".func",
|
|
".funcignore",
|
|
".gitignore",
|
|
}
|
|
|
|
// Builder which creates an OCI-compliant multi-arch (index) container from
|
|
// the function at path.
|
|
type Builder struct {
|
|
name string
|
|
verbose bool
|
|
|
|
onDone func() // optionally provide a function to be notified on done
|
|
buildFn languageLayerBuilder // optionally provide a custom build impl
|
|
}
|
|
|
|
// NewBuilder creates a builder instance.
|
|
func NewBuilder(name string, verbose bool) *Builder {
|
|
return &Builder{name, verbose, nil, nil}
|
|
}
|
|
|
|
func newBuildConfig(ctx context.Context, b *Builder, f fn.Function, platforms []fn.Platform) *buildConfig {
|
|
c := &buildConfig{
|
|
ctx,
|
|
b.name,
|
|
f,
|
|
time.Now(),
|
|
b.verbose,
|
|
"",
|
|
toPlatforms(platforms),
|
|
b.onDone,
|
|
b.buildFn,
|
|
}
|
|
// If the client did not specifically request a certain set of platforms,
|
|
// use the func core defined set of suggested defaults.
|
|
if len(platforms) == 0 {
|
|
c.platforms = toPlatforms(fn.DefaultPlatforms)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// Build an OCI-compliant Mult-arch (v1.ImageIndex) container on disk
|
|
// in the function's runtime data directory at:
|
|
//
|
|
// .func/builds/by-hash/$HASH
|
|
//
|
|
// Updates a symlink to this directory at:
|
|
//
|
|
// .func/builds/last
|
|
func (b *Builder) Build(ctx context.Context, f fn.Function, pp []fn.Platform) (err error) {
|
|
cfg := newBuildConfig(ctx, b, f, pp)
|
|
|
|
if err = setup(cfg); err != nil {
|
|
return
|
|
}
|
|
defer teardown(cfg)
|
|
|
|
// Load the embedded repository
|
|
repo, err := fn.NewRepository("", "")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Write out the scaffolding
|
|
err = scaffolding.Write(cfg.buildDir(), f.Root, f.Runtime, f.Invoke, repo.FS())
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Create an OCI container from the scaffolded function
|
|
if err = containerize(cfg); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = updateLastLink(cfg); err != nil {
|
|
return
|
|
}
|
|
|
|
// TODO: communicating build completeness throgh returning without error
|
|
// relies on the implicit availability of the OIC image in this process'
|
|
// build directory. Would be better to have a formal build result object
|
|
// which includes a general struct which can be used by all builders to
|
|
// communicate to the pusher where the image can be found.
|
|
// Tests, however, can use a simple channel:
|
|
if cfg.onDone != nil {
|
|
cfg.onDone()
|
|
}
|
|
return
|
|
}
|
|
|
|
// buildConfig contains various settings for a single build
|
|
type buildConfig struct {
|
|
ctx context.Context // build context
|
|
name string
|
|
f fn.Function // Function being built
|
|
t time.Time // Timestamp for this build
|
|
verbose bool // verbose logging
|
|
h string // hash cache (use .hash() accessor)
|
|
platforms []v1.Platform
|
|
onDone func() // optionally provide a function to be notified on done
|
|
buildFn languageLayerBuilder // optionally provide a custom build impl
|
|
}
|
|
|
|
func (c *buildConfig) hash() string {
|
|
if c.h != "" {
|
|
return c.h
|
|
}
|
|
var err error
|
|
if c.h, _, err = fn.Fingerprint(c.f.Root); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error calculating fingerprint for build. %v", err)
|
|
}
|
|
return c.h
|
|
}
|
|
|
|
func (c *buildConfig) lastLink() string {
|
|
return path(c.f.Root, fn.RunDataDir, "builds", "last")
|
|
}
|
|
func (c *buildConfig) pidsDir() string {
|
|
return path(c.f.Root, fn.RunDataDir, "builds", "by-pid")
|
|
}
|
|
func (c *buildConfig) pidLink() string {
|
|
return path(c.f.Root, fn.RunDataDir, "builds", "by-pid", strconv.Itoa(os.Getpid()))
|
|
}
|
|
func (c *buildConfig) buildsDir() string {
|
|
return path(c.f.Root, fn.RunDataDir, "builds", "by-hash")
|
|
}
|
|
func (c *buildConfig) buildDir() string {
|
|
return path(c.f.Root, fn.RunDataDir, "builds", "by-hash", c.hash())
|
|
}
|
|
func (c *buildConfig) ociDir() string {
|
|
return path(c.f.Root, fn.RunDataDir, "builds", "by-hash", c.hash(), "oci")
|
|
}
|
|
func (c *buildConfig) blobsDir() string {
|
|
return path(c.f.Root, fn.RunDataDir, "builds", "by-hash", c.hash(), "oci", "blobs", "sha256")
|
|
}
|
|
|
|
func setup(cfg *buildConfig) (err error) {
|
|
if isActive(cfg, cfg.buildDir()) {
|
|
return ErrBuildInProgress{cfg.buildDir()}
|
|
}
|
|
|
|
// create build directory, recreating if it already existed
|
|
if _, err = os.Stat(cfg.buildDir()); !os.IsNotExist(err) {
|
|
if cfg.verbose {
|
|
fmt.Printf("rm -rf %v\n", cfg.buildDir())
|
|
}
|
|
if err = os.RemoveAll(cfg.buildDir()); err != nil {
|
|
return
|
|
}
|
|
}
|
|
if cfg.verbose {
|
|
fmt.Printf("mkdir -p %v\n", cfg.buildDir())
|
|
}
|
|
if err = os.MkdirAll(cfg.buildDir(), 0774); err != nil {
|
|
return
|
|
}
|
|
|
|
// create pid links directory
|
|
if _, err = os.Stat(cfg.pidsDir()); os.IsNotExist(err) {
|
|
if cfg.verbose {
|
|
fmt.Printf("mkdir -p %v\n", cfg.pidsDir())
|
|
}
|
|
if err = os.MkdirAll(cfg.pidsDir(), 0774); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// create a link named $pid to the current build files directory
|
|
target := path("..", "by-hash", cfg.hash())
|
|
if cfg.verbose {
|
|
fmt.Printf("ln -s %v %v\n", target, cfg.pidLink())
|
|
}
|
|
return os.Symlink(target, cfg.pidLink())
|
|
}
|
|
|
|
func teardown(cfg *buildConfig) {
|
|
// remove pid links for processes which no longer exist.
|
|
dd, _ := os.ReadDir(cfg.pidsDir())
|
|
for _, d := range dd {
|
|
if processExists(d.Name()) {
|
|
continue
|
|
}
|
|
dir := path(cfg.pidsDir(), d.Name())
|
|
if cfg.verbose {
|
|
fmt.Printf("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(cfg.buildsDir())
|
|
for _, d := range dd {
|
|
dir := path(cfg.buildsDir(), d.Name())
|
|
if isLinkTo(cfg.lastLink(), dir) {
|
|
continue
|
|
}
|
|
if isActive(cfg, dir) {
|
|
continue
|
|
}
|
|
if cfg.verbose {
|
|
fmt.Printf("rm %v\n", dir)
|
|
}
|
|
_ = os.RemoveAll(dir)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// isActive returns whether or not the given directory path is for a build
|
|
// which is currently active or in progress.
|
|
func isActive(cfg *buildConfig, dir string) bool {
|
|
dd, _ := os.ReadDir(cfg.pidsDir())
|
|
for _, d := range dd {
|
|
link := path(cfg.pidsDir(), d.Name())
|
|
if processExists(d.Name()) && isLinkTo(link, dir) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func updateLastLink(cfg *buildConfig) error {
|
|
if cfg.verbose {
|
|
fmt.Printf("ln -s %v %v\n", cfg.buildDir(), cfg.lastLink())
|
|
}
|
|
_ = os.RemoveAll(cfg.lastLink())
|
|
rp, err := filepath.Rel(filepath.Dir(cfg.lastLink()), cfg.buildDir())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.Symlink(rp, cfg.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
|
|
}
|