func/buildpacks/builder.go

196 lines
5.1 KiB
Go

package buildpacks
import (
"bytes"
"context"
"fmt"
"io"
"os"
"regexp"
"runtime"
"strings"
"github.com/docker/docker/client"
fn "knative.dev/kn-plugin-func"
"knative.dev/kn-plugin-func/docker"
"github.com/Masterminds/semver"
pack "github.com/buildpacks/pack/pkg/client"
"github.com/buildpacks/pack/pkg/logging"
)
// DefaultBuilderImages for Pack builders indexed by Runtime Language
var DefaultBuilderImages = map[string]string{
"node": "gcr.io/paketo-buildpacks/builder:base",
"go": "gcr.io/paketo-buildpacks/builder:base",
}
//Builder holds the configuration that will be passed to
//Buildpack builder
type Builder struct {
verbose bool
}
//NewBuilder builds the new Builder configuration
func NewBuilder(verbose bool) *Builder {
return &Builder{verbose: verbose}
}
var v330 = semver.MustParse("v3.3.0")
// Build the Function at path.
func (builder *Builder) Build(ctx context.Context, f fn.Function) (err error) {
// Use the builder found in the Function configuration file
var packBuilder string
if f.Builder != "" {
packBuilder = f.Builder
pb, ok := f.Builders[packBuilder]
if ok {
packBuilder = pb
}
} else {
packBuilder, err = defaultBuilderImage(f)
if err != nil {
return
}
}
// Build options for the pack client.
var network string
if runtime.GOOS == "linux" {
network = "host"
}
// log output is either STDOUt or kept in a buffer to be printed on error.
var logWriter io.Writer
if builder.verbose {
// pass stdout as non-closeable writer
// otherwise pack client would close it which is bad
logWriter = stdoutWrapper{os.Stdout}
} else {
logWriter = &bytes.Buffer{}
}
cli, dockerHost, err := docker.NewClient(client.DefaultDockerHost)
if err != nil {
return err
}
defer cli.Close()
version, err := cli.ServerVersion(ctx)
if err != nil {
return err
}
var daemonIsPodmanBeforeV330 bool
for _, component := range version.Components {
if component.Name == "Podman Engine" {
v := semver.MustParse(version.Version)
if v.Compare(v330) < 0 {
daemonIsPodmanBeforeV330 = true
}
break
}
}
buildEnvs := make(map[string]string, len(f.BuildEnvs))
for _, env := range f.BuildEnvs {
val, set, err := processEnvValue(*env.Value)
if err != nil {
return err
}
if set {
buildEnvs[*env.Name] = val
}
}
var isTrustedBuilderFunc = func(b string) bool {
return !daemonIsPodmanBeforeV330 &&
(strings.HasPrefix(packBuilder, "quay.io/boson") ||
strings.HasPrefix(packBuilder, "gcr.io/paketo-buildpacks") ||
strings.HasPrefix(packBuilder, "docker.io/paketobuildpacks"))
}
packOpts := pack.BuildOptions{
AppPath: f.Root,
Image: f.Image,
LifecycleImage: "quay.io/boson/lifecycle:0.13.2",
Builder: packBuilder,
Env: buildEnvs,
Buildpacks: f.Buildpacks,
TrustBuilder: isTrustedBuilderFunc,
DockerHost: dockerHost,
ContainerConfig: struct {
Network string
Volumes []string
}{Network: network, Volumes: nil},
}
// Client with a logger which is enabled if in Verbose mode and a dockerClient that supports SSH docker daemon connection.
packClient, err := pack.NewClient(pack.WithLogger(logging.NewSimpleLogger(logWriter)), pack.WithDockerClient(cli))
if err != nil {
return
}
// Build based using the given builder.
if err = packClient.Build(ctx, packOpts); err != nil {
if ctx.Err() != nil {
// received SIGINT
return
} else if !builder.verbose {
// If the builder was not showing logs, embed the full logs in the error.
err = fmt.Errorf("failed to build the function (output: %q): %w", logWriter.(*bytes.Buffer).String(), err)
}
}
return
}
// defaultBuilderImage for the given function based on its runtime, or an
// error if no default is defined for the given runtime.
func defaultBuilderImage(f fn.Function) (string, error) {
v, ok := DefaultBuilderImages[f.Runtime]
if !ok {
return "", fmt.Errorf("Pack builder has no default builder image specified for the '%v' language runtime. Please provide one.", f.Runtime)
}
return v, nil
}
// hack this makes stdout non-closeable
type stdoutWrapper struct {
impl io.Writer
}
func (s stdoutWrapper) Write(p []byte) (n int, err error) {
return s.impl.Write(p)
}
// build command supports only ENV values in from FOO=bar or FOO={{ env:LOCAL_VALUE }}
var buildEnvRegex = regexp.MustCompile(`^{{\s*(\w+)\s*:(\w+)\s*}}$`)
const (
ctxIdx = 1
valIdx = 2
)
// processEnvValue returns only value for ENV variable, that is defined in form FOO=bar or FOO={{ env:LOCAL_VALUE }}
// if the value is correct, it is returned and the second return parameter is set to `true`
// otherwise it is set to `false`
// if the specified value is correct, but the required local variable is not set, error is returned as well
func processEnvValue(val string) (string, bool, error) {
if strings.HasPrefix(val, "{{") {
match := buildEnvRegex.FindStringSubmatch(val)
if len(match) > valIdx && match[ctxIdx] == "env" {
if v, ok := os.LookupEnv(match[valIdx]); ok {
return v, true, nil
} else {
return "", false, fmt.Errorf("required local environment variable %q is not set", match[valIdx])
}
} else {
return "", false, nil
}
}
return val, true, nil
}