Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 192 additions & 16 deletions s2i/builder.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package s2i

import (
"archive/tar"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net/url"
"os"
"path/filepath"

"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"

"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/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/validation"
"github.com/openshift/source-to-image/pkg/build"
Expand All @@ -17,7 +25,7 @@ import (
"github.com/openshift/source-to-image/pkg/scm/git"

fn "knative.dev/kn-plugin-func"
docker "knative.dev/kn-plugin-func/docker"
"knative.dev/kn-plugin-func/docker"
)

var (
Expand All @@ -36,10 +44,17 @@ var DefaultBuilderImages = map[string]string{
"quarkus": "registry.access.redhat.com/ubi8/openjdk-17",
}

// DockerClient is subset of dockerClient.CommonAPIClient required by this package
type DockerClient interface {
ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error)
ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error)
}

// Builder of Functions using the s2i subsystem.
type Builder struct {
verbose bool
impl build.Builder // S2I builder implementation (aka "Strategy")
cli DockerClient
}

type Option func(*Builder)
Expand All @@ -60,6 +75,12 @@ func WithImpl(s build.Builder) Option {
}
}

func WithDockerClient(cli DockerClient) Option {
return func(b *Builder) {
b.cli = cli
}
}

// NewBuilder creates a new instance of a Builder with static defaults.
func NewBuilder(options ...Option) *Builder {
b := &Builder{}
Expand All @@ -70,6 +91,8 @@ func NewBuilder(options ...Option) *Builder {
}

func (b *Builder) Build(ctx context.Context, f fn.Function) (err error) {
// TODO this function currently doesn't support private s2i builder images since credentials are not set

// Builder image from the Function if defined, default otherwise.
builderImage, err := builderImage(f)
if err != nil {
Expand All @@ -87,6 +110,27 @@ func (b *Builder) Build(ctx context.Context, f fn.Function) (err error) {
cfg.RuntimeImagePullPolicy = api.DefaultRuntimeImagePullPolicy
cfg.DockerConfig = s2idocker.GetDefaultDockerConfig()

tmp, err := os.MkdirTemp("", "s2i-build")
if err != nil {
return fmt.Errorf("cannot create temporary dir for s2i build: %w", err)
}
defer os.RemoveAll(tmp)

cfg.AsDockerfile = filepath.Join(tmp, "Dockerfile")

if b.cli == nil {
b.cli, _, err = docker.NewClient(dockerClient.DefaultDockerHost)
if err != nil {
return fmt.Errorf("cannot create docker client: %w", err)
}
}

scriptURL, err := s2iScriptURL(ctx, b.cli, cfg.BuilderImage)
if err != nil {
return fmt.Errorf("cannot get s2i script url: %w", err)
}
cfg.ScriptsURL = scriptURL

// Excludes
// Do not include .git, .env, .func or any language-specific cache directories
// (node_modules, etc) in the tar file sent to the builder, as this both
Expand Down Expand Up @@ -115,20 +159,9 @@ func (b *Builder) Build(ctx context.Context, f fn.Function) (err error) {

// Create the S2I builder instance if not overridden
if b.impl == nil {
var client dockerClient.CommonAPIClient
client, _, err = docker.NewClient(dockerClient.DefaultDockerHost)
if err != nil {
return
}
defer client.Close()

if isPodman(ctx, client) {
client = podmanDockerClient{client}
}

b.impl, _, err = strategies.Strategy(client, cfg, build.Overrides{})
b.impl, _, err = strategies.Strategy(nil, cfg, build.Overrides{})
if err != nil {
return
return fmt.Errorf("cannot create s2i builder: %w", err)
}
}

Expand All @@ -143,7 +176,150 @@ func (b *Builder) Build(ctx context.Context, f fn.Function) (err error) {
fmt.Println(message)
}
}
return

pr, pw := io.Pipe()

go func() {
tw := tar.NewWriter(pw)
err := filepath.Walk(tmp, func(path string, fi fs.FileInfo, err error) error {
if err != nil {
return err
}

p, err := filepath.Rel(tmp, path)
if err != nil {
return fmt.Errorf("cannot get relative path: %w", err)
}

hdr, err := tar.FileInfoHeader(fi, p)
if err != nil {
return fmt.Errorf("cannot create tar header: %w", err)
}
hdr.Name = p

err = tw.WriteHeader(hdr)
if err != nil {
return fmt.Errorf("cannot write header to thar stream: %w", err)
}
if fi.Mode().IsRegular() {
var r io.ReadCloser
r, err = os.Open(path)
if err != nil {
return fmt.Errorf("cannot open source file: %w", err)
}
_, err = io.Copy(tw, r)
if err != nil {
return fmt.Errorf("cannot copy file to tar stream :%w", err)
}
}

return nil
})
_ = tw.Close()
_ = pw.CloseWithError(err)
}()

opts := types.ImageBuildOptions{
Tags: []string{f.Image},
}

resp, err := b.cli.ImageBuild(ctx, pr, opts)
if err != nil {
return fmt.Errorf("cannot build the app image: %w", err)
}
defer resp.Body.Close()

var out io.Writer = io.Discard
if b.verbose {
out = os.Stderr
}

errMsg, err := parseBuildResponse(resp.Body, out)
if err != nil {
return fmt.Errorf("cannot parse response body: %w", err)
}
if errMsg != "" {
return fmt.Errorf("cannot build the app: %s", errMsg)
}

return nil
}

func parseBuildResponse(r io.Reader, w io.Writer) (errorMessage string, err error) {
obj := struct {
ErrorDetail struct {
Message string `json:"message"`
} `json:"errorDetail"`
Stream string `json:"stream"`
}{}
d := json.NewDecoder(r)
for {
err = d.Decode(&obj)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return "", err
}
if obj.ErrorDetail.Message != "" {
errorMessage = obj.ErrorDetail.Message
return errorMessage, nil
}
if obj.Stream != "" {
_, err = w.Write([]byte(obj.Stream))
if err != nil {
return "", err
}
}
}
return "", nil
}

func s2iScriptURL(ctx context.Context, cli DockerClient, image string) (string, error) {
img, _, err := cli.ImageInspectWithRaw(ctx, image)
if err != nil {
if dockerClient.IsErrNotFound(err) { // image is not in the daemon, get info directly from registry
var (
ref name.Reference
img v1.Image
cfg *v1.ConfigFile
)

ref, err = name.ParseReference(image)
if err != nil {
return "", fmt.Errorf("cannot parse image name: %w", err)
}
img, err = remote.Image(ref)
if err != nil {
return "", fmt.Errorf("cannot get image from registry: %w", err)
}
cfg, err = img.ConfigFile()
if err != nil {
return "", fmt.Errorf("cannot get config for image: %w", err)
}

if cfg.Config.Labels != nil {
if u, ok := cfg.Config.Labels["io.openshift.s2i.scripts-url"]; ok {
return u, nil
}
}
}
return "", err
}

if img.Config != nil && img.Config.Labels != nil {
if u, ok := img.Config.Labels["io.openshift.s2i.scripts-url"]; ok {
return u, nil
}
}

if img.ContainerConfig != nil && img.ContainerConfig.Labels != nil {
if u, ok := img.ContainerConfig.Labels["io.openshift.s2i.scripts-url"]; ok {
return u, nil
}
}

return "", nil
}

// builderImage for Function
Expand Down
Loading