mirror of https://github.com/knative/func.git
feat: s2i Go support (#2203)
This commit is contained in:
parent
e6fa020f78
commit
b0418f95bb
|
|
@ -12,6 +12,11 @@
|
|||
/target
|
||||
/hack/bin
|
||||
|
||||
/e2e/testdata/default_home/go
|
||||
/e2e/testdata/default_home/.cache
|
||||
|
||||
/pkg/functions/testdata/migrations/*/.gitignore
|
||||
|
||||
# Nodejs
|
||||
node_modules
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
package s2i
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
fn "knative.dev/func/pkg/functions"
|
||||
)
|
||||
|
||||
// GoAssembler
|
||||
//
|
||||
// Adapted from /usr/libexec/s2i/assemble within the UBI-8 go-toolchain
|
||||
// such that the "go build" command builds subdirectory .s2i/builds/last
|
||||
// (where main resides) rather than the root.
|
||||
// TODO: many apps use the pattern of having main in a subdirectory, for
|
||||
// example the idiomatic "./cmd/myapp/main.go". It would therefore be
|
||||
// beneficial to submit a patch to the go-toolchain source allowing this
|
||||
// path to be customized with an environment variable instead
|
||||
const GoAssembler = `
|
||||
#!/bin/bash
|
||||
set -e
|
||||
pushd /tmp/src
|
||||
if [[ $(go list -f {{.Incomplete}}) == "true" ]]; then
|
||||
INSTALL_URL=${INSTALL_URL:-$IMPORT_URL}
|
||||
if [[ ! -z "$IMPORT_URL" ]]; then
|
||||
popd
|
||||
echo "Assembling GOPATH"
|
||||
export GOPATH=$(realpath $HOME/go)
|
||||
mkdir -p $GOPATH/src/$IMPORT_URL
|
||||
mv /tmp/src/* $GOPATH/src/$IMPORT_URL
|
||||
if [[ -d /tmp/artifacts/pkg ]]; then
|
||||
echo "Restoring previous build artifacts"
|
||||
mv /tmp/artifacts/pkg $GOPATH
|
||||
fi
|
||||
# Resolve dependencies, ignore if vendor present
|
||||
if [[ ! -d $GOPATH/src/$INSTALL_URL/vendor ]]; then
|
||||
echo "Resolving dependencies"
|
||||
pushd $GOPATH/src/$INSTALL_URL
|
||||
go get
|
||||
popd
|
||||
fi
|
||||
# lets build
|
||||
pushd $GOPATH/src/$INSTALL_URL
|
||||
echo "Building"
|
||||
go install -i $INSTALL_URL
|
||||
mv $GOPATH/bin/* /opt/app-root/gobinary
|
||||
popd
|
||||
exit
|
||||
fi
|
||||
exec /$STI_SCRIPTS_PATH/usage
|
||||
else
|
||||
pushd .s2i/builds/last
|
||||
go get f
|
||||
go build -o /opt/app-root/gobinary
|
||||
popd
|
||||
popd
|
||||
fi
|
||||
`
|
||||
|
||||
func assembler(f fn.Function) (string, error) {
|
||||
switch f.Runtime {
|
||||
case "go":
|
||||
return GoAssembler, nil
|
||||
default:
|
||||
return "", fmt.Errorf("no assembler defined for runtime %q", f.Runtime)
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ import (
|
|||
"knative.dev/func/pkg/builders"
|
||||
"knative.dev/func/pkg/docker"
|
||||
fn "knative.dev/func/pkg/functions"
|
||||
"knative.dev/func/pkg/scaffolding"
|
||||
)
|
||||
|
||||
// DefaultName when no WithName option is provided to NewBuilder
|
||||
|
|
@ -41,14 +42,16 @@ const DefaultName = builders.S2I
|
|||
var DefaultNodeBuilder = "registry.access.redhat.com/ubi8/nodejs-20-minimal"
|
||||
var DefaultQuarkusBuilder = "registry.access.redhat.com/ubi8/openjdk-21"
|
||||
var DefaultPythonBuilder = "registry.access.redhat.com/ubi8/python-39"
|
||||
var DefaultGoBuilder = "registry.access.redhat.com/ubi8/go-toolset"
|
||||
|
||||
// DefaultBuilderImages for s2i builders indexed by Runtime Language
|
||||
var DefaultBuilderImages = map[string]string{
|
||||
"go": DefaultGoBuilder,
|
||||
"node": DefaultNodeBuilder,
|
||||
"nodejs": DefaultNodeBuilder,
|
||||
"typescript": DefaultNodeBuilder,
|
||||
"quarkus": DefaultQuarkusBuilder,
|
||||
"python": DefaultPythonBuilder,
|
||||
"quarkus": DefaultQuarkusBuilder,
|
||||
"typescript": DefaultNodeBuilder,
|
||||
}
|
||||
|
||||
// DockerClient is subset of dockerClient.CommonAPIClient required by this package
|
||||
|
|
@ -120,7 +123,8 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
// If a platform was requestd
|
||||
|
||||
// Validate Platforms
|
||||
if len(platforms) == 1 {
|
||||
platform := strings.ToLower(platforms[0].OS + "/" + platforms[0].Architecture)
|
||||
// Try to get the platform image from within the builder image
|
||||
|
|
@ -134,47 +138,6 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf
|
|||
return errors.New("the S2I builder currently only supports specifying a single target platform")
|
||||
}
|
||||
|
||||
// TODO this function currently doesn't support private s2i builder images since credentials are not set
|
||||
|
||||
// Build Config
|
||||
cfg := &api.Config{}
|
||||
cfg.Quiet = !b.verbose
|
||||
cfg.Tag = f.Build.Image
|
||||
cfg.Source = &git.URL{URL: url.URL{Path: f.Root}, Type: git.URLTypeLocal}
|
||||
cfg.BuilderImage = builderImage
|
||||
cfg.BuilderPullPolicy = api.DefaultBuilderPullPolicy
|
||||
cfg.PreviousImagePullPolicy = api.DefaultPreviousImagePullPolicy
|
||||
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)
|
||||
|
||||
funcignorePath := filepath.Join(f.Root, ".funcignore")
|
||||
if _, err := os.Stat(funcignorePath); err == nil {
|
||||
s2iignorePath := filepath.Join(f.Root, ".s2iignore")
|
||||
|
||||
// If the .s2iignore file exists, remove it
|
||||
if _, err := os.Stat(s2iignorePath); err == nil {
|
||||
err := os.Remove(s2iignorePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error removing existing s2iignore file: %w", err)
|
||||
}
|
||||
}
|
||||
// Create the symbolic link
|
||||
err = os.Symlink(funcignorePath, s2iignorePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating symlink: %w", err)
|
||||
}
|
||||
// Removing the symbolic link at the end of the function
|
||||
defer os.Remove(s2iignorePath)
|
||||
}
|
||||
|
||||
cfg.AsDockerfile = filepath.Join(tmp, "Dockerfile")
|
||||
|
||||
var client = b.cli
|
||||
if client == nil {
|
||||
var c dockerClient.CommonAPIClient
|
||||
|
|
@ -186,11 +149,60 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf
|
|||
client = c
|
||||
}
|
||||
|
||||
// Link .s2iignore -> .funcignore
|
||||
funcignorePath := filepath.Join(f.Root, ".funcignore")
|
||||
s2iignorePath := filepath.Join(f.Root, ".s2iignore")
|
||||
if _, err := os.Stat(funcignorePath); err == nil {
|
||||
if _, err := os.Stat(s2iignorePath); err == nil {
|
||||
fmt.Fprintln(os.Stderr, "Warning: an existing .s2iignore was detected. Using this with preference over .funcignore")
|
||||
} else {
|
||||
if err = os.Symlink("./.funcignore", s2iignorePath); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(s2iignorePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Build directory
|
||||
tmp, err := os.MkdirTemp("", "func-s2i-build")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create temporary dir for s2i build: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
// Build Config
|
||||
cfg := &api.Config{
|
||||
Source: &git.URL{
|
||||
Type: git.URLTypeLocal,
|
||||
URL: url.URL{Path: f.Root},
|
||||
},
|
||||
Quiet: !b.verbose,
|
||||
Tag: f.Build.Image,
|
||||
BuilderImage: builderImage,
|
||||
BuilderPullPolicy: api.DefaultBuilderPullPolicy,
|
||||
PreviousImagePullPolicy: api.DefaultPreviousImagePullPolicy,
|
||||
RuntimeImagePullPolicy: api.DefaultRuntimeImagePullPolicy,
|
||||
DockerConfig: s2idocker.GetDefaultDockerConfig(),
|
||||
AsDockerfile: filepath.Join(tmp, "Dockerfile"),
|
||||
}
|
||||
|
||||
// Scaffold
|
||||
if cfg, err = scaffold(cfg, f); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract a an S2I script url from the image if provided and use
|
||||
// this in the build config.
|
||||
scriptURL, err := s2iScriptURL(ctx, client, cfg.BuilderImage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get s2i script url: %w", err)
|
||||
} else if scriptURL != "image:///usr/libexec/s2i" {
|
||||
// Only set if the label found on the image is NOT the default.
|
||||
// Otherwise this label, which is essentially a default fallback, will
|
||||
// take precidence over any scripts provided in ./.s2i/bin, which are
|
||||
// supposed to be the override to that default.
|
||||
cfg.ScriptsURL = scriptURL
|
||||
}
|
||||
cfg.ScriptsURL = scriptURL
|
||||
|
||||
// Excludes
|
||||
// Do not include .git, .env, .func or any language-specific cache directories
|
||||
|
|
@ -218,8 +230,8 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf
|
|||
return errors.New("Unable to build via the s2i builder.")
|
||||
}
|
||||
|
||||
var impl = b.impl
|
||||
// Create the S2I builder instance if not overridden
|
||||
var impl = b.impl
|
||||
if impl == nil {
|
||||
impl, _, err = strategies.Strategy(nil, cfg, build.Overrides{})
|
||||
if err != nil {
|
||||
|
|
@ -235,7 +247,7 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf
|
|||
|
||||
if b.verbose {
|
||||
for _, message := range result.Messages {
|
||||
fmt.Println(message)
|
||||
fmt.Fprintln(os.Stderr, message)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -400,3 +412,56 @@ func BuilderImage(f fn.Function, builderName string) (string, error) {
|
|||
// delegate as the logic is shared amongst builders
|
||||
return builders.Image(f, builderName, DefaultBuilderImages)
|
||||
}
|
||||
|
||||
// scaffold the project
|
||||
// Returns a config with settings suitable for building runtimes which
|
||||
// support scaffolding.
|
||||
func scaffold(cfg *api.Config, f fn.Function) (*api.Config, error) {
|
||||
// Scafffolding is currently only supported by the Go runtime
|
||||
if f.Runtime != "go" {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
contextDir := filepath.Join(".s2i", "builds", "last")
|
||||
appRoot := filepath.Join(f.Root, contextDir)
|
||||
_ = os.RemoveAll(appRoot)
|
||||
|
||||
// The enbedded repository contains the scaffolding code itself which glues
|
||||
// together the middleware and a function via main
|
||||
embeddedRepo, err := fn.NewRepository("", "") // default is the embedded fs
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("unable to load the embedded scaffolding. %w", err)
|
||||
}
|
||||
|
||||
// Write scaffolding to .s2i/builds/last
|
||||
err = scaffolding.Write(appRoot, f.Root, f.Runtime, f.Invoke, embeddedRepo.FS())
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("unable to build due to a scaffold error. %w", err)
|
||||
}
|
||||
|
||||
// Write out an S2I assembler script if the runtime needs to override the
|
||||
// one provided in the S2I image.
|
||||
assemble, err := assembler(f)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if assemble != "" {
|
||||
if err := os.MkdirAll(filepath.Join(f.Root, ".s2i", "bin"), 0755); err != nil {
|
||||
return nil, fmt.Errorf("unable to create .s2i bin dir. %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(f.Root, ".s2i", "bin", "assemble"), []byte(assemble), 0700); err != nil {
|
||||
return nil, fmt.Errorf("unable to write go assembler. %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
cfg.KeepSymlinks = true // Don't infinite loop on the symlink to root.
|
||||
|
||||
// We want to force that the system use the (copy via filesystem)
|
||||
// method rather than a "git clone" method because (other than being
|
||||
// faster) appears to have a bug where the assemble script is ignored.
|
||||
// Maybe this issue is related:
|
||||
// https://github.com/openshift/source-to-image/issues/1141
|
||||
cfg.ForceCopy = true
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import (
|
|||
"knative.dev/func/pkg/builders"
|
||||
"knative.dev/func/pkg/builders/s2i"
|
||||
fn "knative.dev/func/pkg/functions"
|
||||
. "knative.dev/func/pkg/testing"
|
||||
)
|
||||
|
||||
// Test_BuildImages ensures that supported runtimes returns builder image
|
||||
|
|
@ -60,9 +61,9 @@ func Test_BuildImages(t *testing.T) {
|
|||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Without builder - unsupported runtime - go",
|
||||
name: "Without builder - supported runtime - go",
|
||||
function: fn.Function{Runtime: "go"},
|
||||
wantErr: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Without builder - supported runtime - python",
|
||||
|
|
@ -91,17 +92,30 @@ func Test_BuildImages(t *testing.T) {
|
|||
// define a Builder Image will default.
|
||||
func Test_BuilderImageDefault(t *testing.T) {
|
||||
var (
|
||||
i = &mockImpl{} // mock underlying s2i implementation
|
||||
c = mockDocker{} // mock docker client
|
||||
f = fn.Function{Runtime: "node"} // function with no builder image set
|
||||
b = s2i.NewBuilder( // func S2I Builder logic
|
||||
s2i.WithImpl(i), s2i.WithDockerClient(c))
|
||||
root, done = Mktemp(t)
|
||||
runtime = "go"
|
||||
impl = &mockImpl{} // mock the underlying s2i implementation
|
||||
f = fn.Function{
|
||||
Name: "test",
|
||||
Root: root,
|
||||
Runtime: runtime,
|
||||
Registry: "example.com/alice"} // function with no builder image set
|
||||
builder = s2i.NewBuilder( // func S2I Builder logic
|
||||
s2i.WithImpl(impl),
|
||||
s2i.WithDockerClient(mockDocker{}))
|
||||
err error
|
||||
)
|
||||
defer done()
|
||||
|
||||
// An implementation of the underlying S2I implementation which verifies
|
||||
// Initialize the test function
|
||||
if f, err = fn.New().Init(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// An implementation of the underlying S2I builder which verifies
|
||||
// the config has arrived as expected (correct functions logic applied)
|
||||
i.BuildFn = func(cfg *api.Config) (*api.Result, error) {
|
||||
expected := s2i.DefaultBuilderImages["node"]
|
||||
impl.BuildFn = func(cfg *api.Config) (*api.Result, error) {
|
||||
expected := s2i.DefaultBuilderImages[runtime]
|
||||
if cfg.BuilderImage != expected {
|
||||
t.Fatalf("expected s2i config builder image '%v', got '%v'",
|
||||
expected, cfg.BuilderImage)
|
||||
|
|
@ -111,7 +125,7 @@ func Test_BuilderImageDefault(t *testing.T) {
|
|||
|
||||
// Invoke Build, which runs function Builder logic before invoking the
|
||||
// mock impl above.
|
||||
if err := b.Build(context.Background(), f, nil); err != nil {
|
||||
if err := builder.Build(context.Background(), f, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -151,8 +165,8 @@ func Test_BuilderImageConfigurable(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Test_BuildImageWithFuncIgnore ensures that ignored files are not added to the func
|
||||
// image
|
||||
// Test_BuildImageWithFuncIgnore ensures that ignored files are not added to
|
||||
// the func image
|
||||
func Test_BuildImageWithFuncIgnore(t *testing.T) {
|
||||
|
||||
funcIgnoreContent := []byte(`#testing Comments
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ func TestPusher_Push(t *testing.T) {
|
|||
|
||||
// Create and push a function
|
||||
client := fn.New(
|
||||
fn.WithBuilder(NewBuilder("", verbose)),
|
||||
fn.WithBuilder(NewBuilder("", false)),
|
||||
fn.WithPusher(NewPusher(insecure, anon, verbose)))
|
||||
|
||||
f := fn.Function{Root: root, Runtime: "go", Name: "f", Registry: l.Addr().String() + "/funcs"}
|
||||
|
|
@ -85,7 +85,7 @@ func TestPusher_Push(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestPusher_Auth ensures that the pusher authenticates via basic auth when
|
||||
// TestPusher_BasicAuth ensures that the pusher authenticates via basic auth when
|
||||
// supplied with a username/password via the context.
|
||||
func TestPusher_BasicAuth(t *testing.T) {
|
||||
var (
|
||||
|
|
@ -106,7 +106,7 @@ func TestPusher_BasicAuth(t *testing.T) {
|
|||
// no header. ask for auth
|
||||
w.Header().Add("www-authenticate", "Basic realm=\"Registry Realm\"")
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
} else if u != "username" || p != "password" {
|
||||
} else if u != username || p != password {
|
||||
// header exists, but creds are either missing or incorrect
|
||||
t.Fatalf("Unauthorized. Expected user %q pass %q, got user %q pass %q", username, password, u, p)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ func Test_validatePipeline(t *testing.T) {
|
|||
{
|
||||
name: "Unsupported runtime - Go - s2i builder",
|
||||
function: fn.Function{Build: fn.BuildSpec{Builder: builders.S2I}, Runtime: "go"},
|
||||
wantErr: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Supported runtime - Quarkus - pack builder - without additional Buildpacks",
|
||||
|
|
|
|||
Loading…
Reference in New Issue