mirror of https://github.com/knative/func.git
196 lines
5.1 KiB
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
|
|
}
|