Skip to content

Commit

Permalink
feat: s2i Go support
Browse files Browse the repository at this point in the history
  • Loading branch information
lkingland committed May 9, 2024
1 parent 8edcf07 commit 4f40176
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 64 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ EXAMPLES
_ = cmd.Flags().MarkHidden("password")
_ = cmd.Flags().MarkHidden("token")

// Username, Password and Token flags, which plumb through basic auth, are
// currently only available on the experimental "host" builder, which is
// itself behind a feature flag FUNC_ENABLE_HOST_BUILDER. So set these
// flags to hidden until it's out of preview and they are plumbed through
// the docker pusher as well.
cmd.Flags().MarkHidden("username")

Check failure on line 219 in cmd/deploy.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

Error return value of `(*github.com/spf13/pflag.FlagSet).MarkHidden` is not checked (errcheck)
cmd.Flags().MarkHidden("password")

Check failure on line 220 in cmd/deploy.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

Error return value of `(*github.com/spf13/pflag.FlagSet).MarkHidden` is not checked (errcheck)
cmd.Flags().MarkHidden("token")

Check failure on line 221 in cmd/deploy.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

Error return value of `(*github.com/spf13/pflag.FlagSet).MarkHidden` is not checked (errcheck)

// Oft-shared flags:
addConfirmFlag(cmd, cfg.Confirm)
addPathFlag(cmd)
Expand Down
20 changes: 20 additions & 0 deletions cmd/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,32 @@ import (
"github.com/spf13/cobra"

"knative.dev/func/pkg/builders"
"knative.dev/func/pkg/builders/s2i"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/k8s"
"knative.dev/func/pkg/mock"
. "knative.dev/func/pkg/testing"
)

// FIXME: remove this temporary test
// ---------------------------------
func Test_TEMP(t *testing.T) {
// Create a function in a temp directory
root := FromTempDirectory(t) // from
f := fn.Function{Runtime: "go", Root: root, Registry: TestRegistry}
if _, err := fn.New().Init(f); err != nil {
t.Fatal(err)
}

// Deploy
cmd := NewDeployCmd(NewTestClient(fn.WithBuilder(s2i.NewBuilder())))
cmd.SetArgs([]string{"--builder", "s2i"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}

}

// commandConstructor is used to share test implementations between commands
// which only differ in the command being tested (ex: build and deploy share
// a large overlap in tests because building is a subset of the deploy task)
Expand Down
66 changes: 66 additions & 0 deletions pkg/builders/s2i/assemblers.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
157 changes: 110 additions & 47 deletions pkg/builders/s2i/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -186,11 +149,58 @@ 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 preferance over .funcignore")

Check failure on line 157 in pkg/builders/s2i/builder.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

[github.com/client9/misspell] reported by reviewdog 🐶 "preferance" is a misspelling of "preference" Raw Output: pkg/builders/s2i/builder.go:157:91: "preferance" is a misspelling of "preference"
} else {
os.Symlink("./.funcignore", s2iignorePath)

Check failure on line 159 in pkg/builders/s2i/builder.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

Error return value of `os.Symlink` is not checked (errcheck)
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
// suposed to be the override to that default.

Check failure on line 201 in pkg/builders/s2i/builder.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

[github.com/client9/misspell] reported by reviewdog 🐶 "suposed" is a misspelling of "supposed" Raw Output: pkg/builders/s2i/builder.go:201:5: "suposed" is a misspelling of "supposed"
cfg.ScriptsURL = scriptURL
}
cfg.ScriptsURL = scriptURL

// Excludes
// Do not include .git, .env, .func or any language-specific cache directories
Expand Down Expand Up @@ -218,8 +228,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 {
Expand All @@ -235,7 +245,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.Fprintf(os.Stderr, message)

Check failure on line 248 in pkg/builders/s2i/builder.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

SA1006: printf-style function with dynamic format string and no further arguments should use print-style function instead (staticcheck)
}
}

Expand Down Expand Up @@ -400,3 +410,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
}

0 comments on commit 4f40176

Please sign in to comment.