mirror of https://github.com/knative/func.git
feat: host oci builder (#1730)
* feat: oci builder for host builds * do not expose host builder until fully baked
This commit is contained in:
parent
64337b4df5
commit
9a790f005f
24
cmd/build.go
24
cmd/build.go
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"knative.dev/func/pkg/builders"
|
"knative.dev/func/pkg/builders"
|
||||||
"knative.dev/func/pkg/builders/buildpacks"
|
pack "knative.dev/func/pkg/builders/buildpacks"
|
||||||
"knative.dev/func/pkg/builders/s2i"
|
"knative.dev/func/pkg/builders/s2i"
|
||||||
"knative.dev/func/pkg/config"
|
"knative.dev/func/pkg/config"
|
||||||
fn "knative.dev/func/pkg/functions"
|
fn "knative.dev/func/pkg/functions"
|
||||||
|
@ -169,25 +169,21 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro
|
||||||
|
|
||||||
// Client
|
// Client
|
||||||
// Concrete implementations (ex builder) vary based on final effective config
|
// Concrete implementations (ex builder) vary based on final effective config
|
||||||
var builder fn.Builder
|
var client *fn.Client
|
||||||
|
o := []fn.Option{fn.WithRegistry(cfg.Registry)}
|
||||||
if f.Build.Builder == builders.Pack {
|
if f.Build.Builder == builders.Pack {
|
||||||
builder = buildpacks.NewBuilder(
|
o = append(o, fn.WithBuilder(pack.NewBuilder(
|
||||||
buildpacks.WithName(builders.Pack),
|
pack.WithName(builders.Pack),
|
||||||
buildpacks.WithVerbose(cfg.Verbose),
|
pack.WithTimestamp(cfg.WithTimestamp),
|
||||||
buildpacks.WithTimestamp(cfg.WithTimestamp),
|
pack.WithVerbose(cfg.Verbose))))
|
||||||
)
|
|
||||||
} else if f.Build.Builder == builders.S2I {
|
} else if f.Build.Builder == builders.S2I {
|
||||||
builder = s2i.NewBuilder(
|
o = append(o, fn.WithBuilder(s2i.NewBuilder(
|
||||||
s2i.WithName(builders.S2I),
|
s2i.WithName(builders.S2I),
|
||||||
s2i.WithPlatform(cfg.Platform),
|
s2i.WithPlatform(cfg.Platform),
|
||||||
s2i.WithVerbose(cfg.Verbose))
|
s2i.WithVerbose(cfg.Verbose))))
|
||||||
} else {
|
|
||||||
return builders.ErrUnknownBuilder{Name: f.Build.Builder, Known: KnownBuilders()}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client, done := newClient(ClientConfig{Verbose: cfg.Verbose},
|
client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, o...)
|
||||||
fn.WithRegistry(cfg.Registry),
|
|
||||||
fn.WithBuilder(builder))
|
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
// Build and (optionally) push
|
// Build and (optionally) push
|
||||||
|
|
|
@ -387,8 +387,7 @@ func (c *Client) Registry() string {
|
||||||
func (c *Client) Runtimes() ([]string, error) {
|
func (c *Client) Runtimes() ([]string, error) {
|
||||||
runtimes := utils.NewSortedSet()
|
runtimes := utils.NewSortedSet()
|
||||||
|
|
||||||
// Gather all runtimes from all repositories
|
// Gather all runtimes from all repositories into a uniqueness map
|
||||||
// into a uniqueness map
|
|
||||||
repositories, err := c.Repositories().All()
|
repositories, err := c.Repositories().All()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return []string{}, err
|
||||||
|
@ -1026,7 +1025,7 @@ func ensureRunDataDir(root string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fingerprint the files at a given path. Returns a hash calculated from the
|
// Fingerprint the files at a given path. Returns a hash calculated from the
|
||||||
// filenames and modification timestamps of the files within the given root.
|
// filenames and modification timestamps of the files within the given root.
|
||||||
// Also returns a logfile consiting of the filenames and modification times
|
// Also returns a logfile consiting of the filenames and modification times
|
||||||
// which contributed to the hash.
|
// which contributed to the hash.
|
||||||
|
@ -1035,7 +1034,7 @@ func ensureRunDataDir(root string) error {
|
||||||
// .git and .func.
|
// .git and .func.
|
||||||
// Future updates will include files explicitly marked as ignored by a
|
// Future updates will include files explicitly marked as ignored by a
|
||||||
// .funcignore.
|
// .funcignore.
|
||||||
func fingerprint(root string) (hash, log string, err error) {
|
func Fingerprint(root string) (hash, log string, err error) {
|
||||||
h := sha256.New() // Hash builder
|
h := sha256.New() // Hash builder
|
||||||
l := bytes.Buffer{} // Log buffer
|
l := bytes.Buffer{} // Log buffer
|
||||||
|
|
||||||
|
|
|
@ -415,7 +415,7 @@ func (f Function) Stamp(oo ...stampOption) (err error) {
|
||||||
|
|
||||||
// Cacluate the hash and a logfile of what comprised it
|
// Cacluate the hash and a logfile of what comprised it
|
||||||
var hash, log string
|
var hash, log string
|
||||||
if hash, log, err = fingerprint(f.Root); err != nil {
|
if hash, log, err = Fingerprint(f.Root); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -633,7 +633,7 @@ func (f Function) Built() bool {
|
||||||
// It's a pretty good chance the thing doesn't need to be rebuilt, though
|
// It's a pretty good chance the thing doesn't need to be rebuilt, though
|
||||||
// of course filesystem racing conditions do exist, including both direct
|
// of course filesystem racing conditions do exist, including both direct
|
||||||
// source code modifications or changes to the image cache.
|
// source code modifications or changes to the image cache.
|
||||||
hash, _, err := fingerprint(f.Root)
|
hash, _, err := Fingerprint(f.Root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error calculating function's fingerprint: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error calculating function's fingerprint: %v\n", err)
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -0,0 +1,276 @@
|
||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
|
||||||
|
fn "knative.dev/func/pkg/functions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var path = filepath.Join
|
||||||
|
|
||||||
|
// TODO: This may no longer be necessary, delete if e2e and acceptance tests
|
||||||
|
// succeed:
|
||||||
|
// const DefaultName = builders.Host
|
||||||
|
|
||||||
|
var defaultPlatforms = []v1.Platform{
|
||||||
|
{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"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultIgnored = []string{ // TODO: implement and use .funcignore
|
||||||
|
".git",
|
||||||
|
".func",
|
||||||
|
".funcignore",
|
||||||
|
".gitignore",
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildErr indicates a build error occurred.
|
||||||
|
type BuildErr struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e BuildErr) Error() string {
|
||||||
|
return fmt.Sprintf("error performing host build. %v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder which creates an OCI-compliant multi-arch (index) container from
|
||||||
|
// the function at path.
|
||||||
|
type Builder struct {
|
||||||
|
name string
|
||||||
|
client *fn.Client
|
||||||
|
verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuilder creates a builder instance.
|
||||||
|
func NewBuilder(name string, client *fn.Client, verbose bool) *Builder {
|
||||||
|
return &Builder{name, client, verbose}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) (err error) {
|
||||||
|
cfg := &buildConfig{ctx, b.client, f, time.Now(), b.verbose, ""}
|
||||||
|
|
||||||
|
if err = setup(cfg); err != nil { // create directories and links
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer teardown(cfg)
|
||||||
|
|
||||||
|
//TODO: Use clien't actual Scaffold when ready:
|
||||||
|
/*
|
||||||
|
if err = cfg.client.Scaffold(ctx, f, cfg.buildDir()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// IN the meantime, use an airball mainfile
|
||||||
|
data := `
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func main () {
|
||||||
|
fmt.Println("Hello, world!")
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err = os.WriteFile(path(cfg.buildDir(), "main.go"), []byte(data), 0664); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = containerize(cfg); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return updateLastLink(cfg)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildConfig contains various settings for a single build
|
||||||
|
type buildConfig struct {
|
||||||
|
ctx context.Context // build context
|
||||||
|
client *fn.Client // backreference to the client for Scaffolding
|
||||||
|
f fn.Function // Function being built
|
||||||
|
t time.Time // Timestamp for this build
|
||||||
|
verbose bool // verbose logging
|
||||||
|
h string // hash cache (use .hash() accessor)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup errors if there already exists a build directory. Otherwise, it
|
||||||
|
// creates a build directory based on the function's hash, and creates
|
||||||
|
// a link to this build directory for the current pid to denote the build
|
||||||
|
// is in progress.
|
||||||
|
func setup(cfg *buildConfig) (err error) {
|
||||||
|
// error if already in progress
|
||||||
|
if isActive(cfg, cfg.buildDir()) {
|
||||||
|
return BuildErr{fmt.Errorf("Build directory already exists for this version hash and is associated with an active PID. Is a build already in progress? %v", cfg.buildDir())}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create build files directory
|
||||||
|
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 the pid link for the current process indicating the build is
|
||||||
|
// no longer in progress.
|
||||||
|
_ = os.RemoveAll(cfg.pidLink())
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
return os.Symlink(cfg.buildDir(), cfg.lastLink())
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
fn "knative.dev/func/pkg/functions"
|
||||||
|
. "knative.dev/func/pkg/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestBuilder ensures that, when given a Go Function, an OCI-compliant
|
||||||
|
// directory structure is created on .Build in the expected path.
|
||||||
|
func TestBuilder(t *testing.T) {
|
||||||
|
root, done := Mktemp(t)
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
client := fn.New()
|
||||||
|
|
||||||
|
f, err := client.Init(fn.Function{Root: root, Runtime: "go"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := NewBuilder("", client, true)
|
||||||
|
|
||||||
|
if err := builder.Build(context.Background(), f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
last := path(f.Root, fn.RunDataDir, "builds", "last", "oci")
|
||||||
|
|
||||||
|
validateOCI(last, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageIndex represents the structure of an OCI Image Index.
|
||||||
|
type ImageIndex struct {
|
||||||
|
SchemaVersion int `json:"schemaVersion"`
|
||||||
|
Manifests []struct {
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
Platform struct {
|
||||||
|
Architecture string `json:"architecture"`
|
||||||
|
OS string `json:"os"`
|
||||||
|
} `json:"platform"`
|
||||||
|
} `json:"manifests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateOCI performs a cursory check that the given path exists and
|
||||||
|
// has the basics of an OCI compliant structure.
|
||||||
|
func validateOCI(path string, t *testing.T) {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
t.Fatalf("unable to stat output path. %v", path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ociLayoutFile := filepath.Join(path, "oci-layout")
|
||||||
|
indexJSONFile := filepath.Join(path, "index.json")
|
||||||
|
blobsDir := filepath.Join(path, "blobs")
|
||||||
|
|
||||||
|
// Check if required files and directories exist
|
||||||
|
if _, err := os.Stat(ociLayoutFile); os.IsNotExist(err) {
|
||||||
|
t.Fatal("missing oci-layout file")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(indexJSONFile); os.IsNotExist(err) {
|
||||||
|
t.Fatal("missing index.json file")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(blobsDir); os.IsNotExist(err) {
|
||||||
|
t.Fatal("missing blobs directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and validate index.json
|
||||||
|
indexJSONData, err := os.ReadFile(indexJSONFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read index.json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageIndex ImageIndex
|
||||||
|
err = json.Unmarshal(indexJSONData, &imageIndex)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse index.json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if imageIndex.SchemaVersion != 2 {
|
||||||
|
t.Fatalf("invalid schema version, expected 2, got %d", imageIndex.SchemaVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional validation of the Image Index structure can be added here
|
||||||
|
// extract. for example checking that the path includes the README.md
|
||||||
|
// and one of the binaries in the exact location expected (the data layer
|
||||||
|
// blob and exec layer blob, respectively)
|
||||||
|
}
|
|
@ -0,0 +1,388 @@
|
||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// languageLayerBuilder builds the layer for the given language whuch may
|
||||||
|
// be different from one platform to another. For example, this is the
|
||||||
|
// layer in the image which contains the Go cross-compiled binary.
|
||||||
|
type languageLayerBuilder interface {
|
||||||
|
Build(*buildConfig, v1.Platform) (v1.Descriptor, v1.Layer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLanguageLayerBuilder(cfg *buildConfig) (l languageLayerBuilder, err error) {
|
||||||
|
switch cfg.f.Runtime {
|
||||||
|
case "go":
|
||||||
|
l = goLayerBuilder{}
|
||||||
|
case "python":
|
||||||
|
// Likely the next to be supported after Go
|
||||||
|
err = errors.New("functions written in Python are not yet supported by the host builder")
|
||||||
|
case "node":
|
||||||
|
// Likely the next to be supported after Python
|
||||||
|
err = errors.New("functions written in Node are not yet supported by the host builder")
|
||||||
|
case "rust":
|
||||||
|
// Likely the next to be supprted after Node
|
||||||
|
err = errors.New("functions written in Rust are not yet supported by the host builder")
|
||||||
|
default:
|
||||||
|
// Others are not likely to be supported in the near future without
|
||||||
|
// increased contributions.
|
||||||
|
err = fmt.Errorf("the language runtime '%v' is not a recognized language by the host builder", cfg.f.Runtime)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// containerize the scaffolded project by creating and writing an OCI
|
||||||
|
// conformant directory structure into the functions .func/builds directory.
|
||||||
|
// The source code to be containerized is indicated by cfg.dir
|
||||||
|
func containerize(cfg *buildConfig) (err error) {
|
||||||
|
// Create the required directories: oci/blobs/sha256
|
||||||
|
if err = os.MkdirAll(cfg.blobsDir(), os.ModePerm); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the static, required oci-layout metadata file
|
||||||
|
if err = os.WriteFile(path(cfg.ociDir(), "oci-layout"),
|
||||||
|
[]byte(`{ "imageLayoutVersion": "1.0.0" }`), os.ModePerm); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the data layer and its descriptor
|
||||||
|
dataDesc, dataLayer, err := newDataLayer(cfg) // shared
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: if the base image is not provided, create a certificates layer
|
||||||
|
// which includes root certificates such that the resultant container
|
||||||
|
// can validate SSL (make HTTPS requests)
|
||||||
|
/*
|
||||||
|
certsDesc, certsLayer, err := newCerts(cfg) // shared
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Create an image for each platform consisting of the shared data layer
|
||||||
|
// and an os/platform specific layer.
|
||||||
|
imageDescs := []v1.Descriptor{}
|
||||||
|
for _, p := range defaultPlatforms { // TODO: Configurable additions.
|
||||||
|
imageDesc, err := newImage(cfg, dataDesc, dataLayer, p, cfg.verbose)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
imageDescs = append(imageDescs, imageDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Image Index which enumerates all images contained within
|
||||||
|
// the container.
|
||||||
|
_, err = newImageIndex(cfg, imageDescs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDataLayer creates the shared data layer in the container file hierarchy and
|
||||||
|
// returns both its descriptor and layer metadata.
|
||||||
|
func newDataLayer(cfg *buildConfig) (desc v1.Descriptor, layer v1.Layer, err error) {
|
||||||
|
|
||||||
|
// Create the data tarball
|
||||||
|
// TODO: try WithCompressedCaching?
|
||||||
|
source := cfg.f.Root // The source is the function's entire filesystem
|
||||||
|
target := path(cfg.buildDir(), "datalayer.tar.gz")
|
||||||
|
|
||||||
|
if err = newDataTarball(source, target, defaultIgnored, cfg.verbose); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layer
|
||||||
|
if layer, err = tarball.LayerFromFile(target); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptor
|
||||||
|
if desc, err = newDescriptor(layer); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blob
|
||||||
|
blob := path(cfg.blobsDir(), desc.Digest.Hex)
|
||||||
|
if cfg.verbose {
|
||||||
|
fmt.Printf("mv %v %v\n", rel(cfg.buildDir(), target), rel(cfg.buildDir(), blob))
|
||||||
|
}
|
||||||
|
err = os.Rename(target, blob)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDataTarball(source, 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(source, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range ignored {
|
||||||
|
if info.Name() == v {
|
||||||
|
if info.IsDir() {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := tar.FileInfoHeader(info, info.Name())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(source, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
header.Name = filepath.Join("/func", relPath)
|
||||||
|
// TODO: should we set file timestamps to the build start time of cfg.t?
|
||||||
|
// header.ModTime = timestampArgument
|
||||||
|
|
||||||
|
if err := tw.WriteHeader(header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("→ %v \n", header.Name)
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(tw, file)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// newImage creates an image for the given platform.
|
||||||
|
// The image consists of the shared data layer which is provided
|
||||||
|
func newImage(cfg *buildConfig, dataDesc v1.Descriptor, dataLayer v1.Layer, p v1.Platform, verbose bool) (imageDesc v1.Descriptor, err error) {
|
||||||
|
b, err := newLanguageLayerBuilder(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Exec Layer as Blob -> Layer
|
||||||
|
execDesc, execLayer, err := b.Build(cfg, p)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Config Layer as Blob -> Layer
|
||||||
|
configDesc, _, err := newConfig(cfg, p, dataLayer, execLayer)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image Manifest
|
||||||
|
image := v1.Manifest{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
MediaType: types.OCIManifestSchema1,
|
||||||
|
Config: configDesc,
|
||||||
|
Layers: []v1.Descriptor{dataDesc, execDesc},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write image manifest out as json to a tempfile
|
||||||
|
filePath := fmt.Sprintf("image.%v.%v.json", p.OS, p.Architecture)
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(file)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
if err = enc.Encode(image); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = file.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a descriptor from hash and size
|
||||||
|
file, err = os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash, size, err := v1.SHA256(file)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imageDesc = v1.Descriptor{
|
||||||
|
MediaType: types.OCIManifestSchema1,
|
||||||
|
Digest: hash,
|
||||||
|
Size: size,
|
||||||
|
Platform: &p,
|
||||||
|
}
|
||||||
|
if err = file.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// move image into blobs
|
||||||
|
blob := path(cfg.blobsDir(), hash.Hex)
|
||||||
|
if cfg.verbose {
|
||||||
|
fmt.Printf("mv %v %v\n", rel(cfg.buildDir(), filePath), rel(cfg.buildDir(), blob))
|
||||||
|
}
|
||||||
|
err = os.Rename(filePath, blob)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfig(cfg *buildConfig, p v1.Platform, layers ...v1.Layer) (desc v1.Descriptor, config v1.ConfigFile, err error) {
|
||||||
|
volumes := make(map[string]struct{}) // Volumes are odd, see spec.
|
||||||
|
for _, v := range cfg.f.Run.Volumes {
|
||||||
|
if v.Path == nil {
|
||||||
|
continue // TODO: remove pointers from Volume and Env struct members
|
||||||
|
}
|
||||||
|
volumes[*v.Path] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootfs := v1.RootFS{
|
||||||
|
Type: "layers",
|
||||||
|
}
|
||||||
|
var diff v1.Hash
|
||||||
|
for _, v := range layers {
|
||||||
|
if diff, err = v.DiffID(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rootfs.DiffIDs = append(rootfs.DiffIDs, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
config = v1.ConfigFile{
|
||||||
|
Created: v1.Time{Time: cfg.t},
|
||||||
|
Architecture: p.Architecture,
|
||||||
|
OS: p.OS,
|
||||||
|
OSVersion: p.OSVersion,
|
||||||
|
// OSFeatures: p.OSFeatures, // TODO: need to update dep to get this
|
||||||
|
Variant: p.Variant,
|
||||||
|
Config: v1.Config{
|
||||||
|
ExposedPorts: map[string]struct{}{"8080/tcp": {}},
|
||||||
|
Env: cfg.f.Run.Envs.Slice(),
|
||||||
|
Cmd: []string{"/func/f"}, // NOTE: Using Cmd because Entrypoint can not be overridden
|
||||||
|
WorkingDir: "/func/",
|
||||||
|
StopSignal: "SIGKILL",
|
||||||
|
Volumes: volumes,
|
||||||
|
// Labels
|
||||||
|
// History
|
||||||
|
},
|
||||||
|
RootFS: rootfs,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the config out as json to a tempfile
|
||||||
|
filePath := path(cfg.buildDir(), "config.json")
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(file)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
if err = enc.Encode(config); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = file.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a descriptor using hash and size
|
||||||
|
file, err = os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash, size, err := v1.SHA256(file)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
desc = v1.Descriptor{
|
||||||
|
MediaType: types.OCIConfigJSON,
|
||||||
|
Digest: hash,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
if err = file.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// move config into blobs
|
||||||
|
blobPath := path(cfg.blobsDir(), hash.Hex)
|
||||||
|
if cfg.verbose {
|
||||||
|
fmt.Printf("mv %v %v\n", rel(cfg.buildDir(), filePath), rel(cfg.buildDir(), blobPath))
|
||||||
|
}
|
||||||
|
err = os.Rename(filePath, blobPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newImageIndex(cfg *buildConfig, imageDescs []v1.Descriptor) (index v1.IndexManifest, err error) {
|
||||||
|
index = v1.IndexManifest{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
MediaType: types.OCIImageIndex,
|
||||||
|
Manifests: imageDescs,
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := path(cfg.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||||
|
fn "knative.dev/func/pkg/functions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type goLayerBuilder struct{}
|
||||||
|
|
||||||
|
// Build the source code as Go, cross compiled for the given platform, placing
|
||||||
|
// the statically linked binary in a tarred layer and return the Descriptor
|
||||||
|
// and Layer metadata.
|
||||||
|
func (c goLayerBuilder) Build(cfg *buildConfig, p v1.Platform) (desc v1.Descriptor, layer v1.Layer, err error) {
|
||||||
|
|
||||||
|
// Executable
|
||||||
|
exe, err := goBuild(cfg, p) // Compile binary returning its path
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tarball
|
||||||
|
target := path(cfg.buildDir(), fmt.Sprintf("execlayer.%v.%v.tar.gz", p.OS, p.Architecture))
|
||||||
|
if err = newExecTarball(exe, target, cfg.verbose); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layer
|
||||||
|
if layer, err = tarball.LayerFromFile(target); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptor
|
||||||
|
if desc, err = newDescriptor(layer); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
desc.Platform = &p
|
||||||
|
|
||||||
|
// Blob
|
||||||
|
blob := path(cfg.blobsDir(), desc.Digest.Hex)
|
||||||
|
if cfg.verbose {
|
||||||
|
fmt.Printf("mv %v %v\n", rel(cfg.buildDir(), target), rel(cfg.buildDir(), blob))
|
||||||
|
}
|
||||||
|
err = os.Rename(target, blob)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func goBuild(cfg *buildConfig, p v1.Platform) (binPath string, err error) {
|
||||||
|
gobin, args, outpath, err := goBuildCmd(p, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
envs := goBuildEnvs(cfg.f, p)
|
||||||
|
if cfg.verbose {
|
||||||
|
fmt.Printf("%v %v\n", gobin, strings.Join(args, " "))
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %v\n", filepath.Base(outpath))
|
||||||
|
}
|
||||||
|
cmd := exec.CommandContext(cfg.ctx, gobin, args...)
|
||||||
|
cmd.Env = envs
|
||||||
|
cmd.Dir = cfg.buildDir()
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
|
||||||
|
return outpath, cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func goBuildCmd(p v1.Platform, cfg *buildConfig) (gobin string, args []string, outpath string, err error) {
|
||||||
|
/* TODO: Use Build Command override from the function if provided
|
||||||
|
* A future PR will include the ability to specify a
|
||||||
|
* f.Build.BuildCommand, or BuildArgs for use here to customize
|
||||||
|
* This will be useful when, for example, the function is written in
|
||||||
|
* Go and the function developer needs Libc compatibility, in which case
|
||||||
|
* the default command will need to be replaced with:
|
||||||
|
* go build -ldflags "-linkmode 'external' -extldflags '-static'"
|
||||||
|
* Pseudocode:
|
||||||
|
* if BuildArgs or BuildCommand
|
||||||
|
* Validate command or args are safe to run
|
||||||
|
* no other commands injected
|
||||||
|
* does not contain Go's "toolexec"
|
||||||
|
* does not specify the output path
|
||||||
|
* Either replace or append to gobin
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Use the binary specified FUNC_GO_PATH if defined
|
||||||
|
gobin = os.Getenv("FUNC_GO_PATH") // TODO: move to main and plumb through
|
||||||
|
if gobin == "" {
|
||||||
|
gobin = "go"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build as ./func/builds/$PID/result/f.$OS.$Architecture
|
||||||
|
name := fmt.Sprintf("f.%v.%v", p.OS, p.Architecture)
|
||||||
|
if p.Variant != "" {
|
||||||
|
name = name + "." + p.Variant
|
||||||
|
}
|
||||||
|
outpath = path(cfg.buildDir(), "result", name)
|
||||||
|
args = []string{"build", "-o", outpath}
|
||||||
|
return gobin, args, outpath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func goBuildEnvs(f fn.Function, p v1.Platform) (envs []string) {
|
||||||
|
pegged := []string{
|
||||||
|
"CGO_ENABLED=0",
|
||||||
|
"GOOS=" + p.OS,
|
||||||
|
"GOARCH=" + p.Architecture,
|
||||||
|
}
|
||||||
|
if p.Variant != "" && p.Architecture == "arm" {
|
||||||
|
pegged = append(pegged, "GOARM="+strings.TrimPrefix(p.Variant, "v"))
|
||||||
|
} else if p.Variant != "" && p.Architecture == "amd64" {
|
||||||
|
pegged = append(pegged, "GOAMD64="+p.Variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
isPegged := func(env string) bool {
|
||||||
|
for _, v := range pegged {
|
||||||
|
name := strings.Split(v, "=")[0]
|
||||||
|
if strings.HasPrefix(env, name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
envs = append(envs, pegged...)
|
||||||
|
for _, env := range os.Environ() {
|
||||||
|
if !isPegged(env) {
|
||||||
|
envs = append(envs, env)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return envs
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExecTarball(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()
|
||||||
|
|
||||||
|
info, err := os.Stat(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := tar.FileInfoHeader(info, info.Name())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
header.Name = path("/func", "f")
|
||||||
|
// TODO: should we set file timestamps to the build start time of cfg.t?
|
||||||
|
// header.ModTime = timestampArgument
|
||||||
|
|
||||||
|
if err = tw.WriteHeader(header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("→ %v \n", header.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
i, err := io.Copy(tw, file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf(" wrote %v bytes \n", i)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue