Skip to content

Commit

Permalink
additional configuration and tests
Browse files Browse the repository at this point in the history
- configurable platforms for build tasks
- additional typed errors and reorganizes
- expands test cases to check typed errors are used
- configurable global and per-run startup timeout
- tests run readiness endpoint is considered and respects timeouts
- adds a concurrency test which ensures only one build executes per
  process
- passes the --platform flag through as a build option
  • Loading branch information
lkingland committed Jun 5, 2023
1 parent 4efbad7 commit 22b077a
Show file tree
Hide file tree
Showing 33 changed files with 905 additions and 221 deletions.
27 changes: 25 additions & 2 deletions cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,18 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro
} else if f.Build.Builder == builders.S2I {
o = append(o, fn.WithBuilder(s2i.NewBuilder(
s2i.WithName(builders.S2I),
s2i.WithPlatform(cfg.Platform),
s2i.WithVerbose(cfg.Verbose))))
}

client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, o...)
defer done()

// Build and (optionally) push
if f, err = client.Build(cmd.Context(), f); err != nil {
buildOptions, err := cfg.buildOptions()
if err != nil {
return
}
if f, err = client.Build(cmd.Context(), f, buildOptions...); err != nil {
return
}
if cfg.Push {
Expand Down Expand Up @@ -233,6 +236,26 @@ type buildConfig struct {
WithTimestamp bool
}

func (c buildConfig) buildOptions() (oo []fn.BuildOption, err error) {
oo = []fn.BuildOption{}

// Platforms
//
// TODO: upgrade --platform to a multi-value field. The individual builder
// implementations are responsible for bubbling an error if they do
// not support this. Pack supports none, S2I supports one, host builder
// supports multi.
if c.Platform != "" {
parts := strings.Split(c.Platform, "/")
if len(parts) != 2 {
return oo, fmt.Errorf("the value for --patform must be in the form [OS]/[Architecture]. eg \"linux/amd64\"")
}
oo = append(oo, fn.BuildWithPlatforms([]fn.Platform{{OS: parts[0], Architecture: parts[1]}}))
}

return
}

// newBuildConfig gathers options into a single build request.
func newBuildConfig() buildConfig {
return buildConfig{
Expand Down
10 changes: 7 additions & 3 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,6 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) {
} else if f.Build.Builder == builders.S2I {
builder = s2i.NewBuilder(
s2i.WithName(builders.S2I),
s2i.WithPlatform(cfg.Platform),
s2i.WithVerbose(cfg.Verbose))
} else {
return builders.ErrUnknownBuilder{Name: f.Build.Builder, Known: KnownBuilders()}
Expand All @@ -283,8 +282,13 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) {
}
} else {
if shouldBuild(cfg.Build, f, client) { // --build or "auto" with FS changes
if f, err = client.Build(cmd.Context(), f); err != nil {
return
buildOptions, err := cfg.buildOptions()
if err != nil {
return err
}

if f, err = client.Build(cmd.Context(), f, buildOptions...); err != nil {
return err
}
}
if cfg.Push {
Expand Down
9 changes: 6 additions & 3 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ func runRun(cmd *cobra.Command, args []string, newClient ClientFactory) (err err
} else if f.Build.Builder == builders.S2I {
o = append(o, fn.WithBuilder(s2i.NewBuilder(
s2i.WithName(builders.S2I),
s2i.WithPlatform(cfg.Platform),
s2i.WithVerbose(cfg.Verbose))))
}
if cfg.Container {
Expand All @@ -194,8 +193,12 @@ func runRun(cmd *cobra.Command, args []string, newClient ClientFactory) (err err
// If requesting to run via the container, build the container if it is
// either out-of-date or a build was explicitly requested.
if cfg.Container && shouldBuild(cfg.Build, f, client) {
if f, err = client.Build(cmd.Context(), f); err != nil {
return
buildOptions, err := cfg.buildOptions()
if err != nil {
return err
}
if f, err = client.Build(cmd.Context(), f, buildOptions...); err != nil {
return err
}
}

Expand Down
8 changes: 7 additions & 1 deletion pkg/builders/buildpacks/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package buildpacks
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"runtime"
Expand Down Expand Up @@ -108,7 +109,12 @@ func WithTimestamp(v bool) Option {
var DefaultLifecycleImage = "quay.io/boson/lifecycle@sha256:f53fea9ec9188b92cab0b8a298ff852d76a6c2aaf56f968a08637e13de0e0c59"

// Build the Function at path.
func (b *Builder) Build(ctx context.Context, f fn.Function) (err error) {
func (b *Builder) Build(ctx context.Context, f fn.Function, oo ...fn.BuildOption) (err error) {
options := fn.NewBuildOptions(oo...)
if len(options.Platforms) != 0 {
return errors.New("the pack builder does not support specifying target platforms directly.")
}

// Builder image from the function if defined, default otherwise.
image, err := BuilderImage(f, b.name)
if err != nil {
Expand Down
38 changes: 22 additions & 16 deletions pkg/builders/s2i/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,10 @@ type DockerClient interface {

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

type Option func(*Builder)
Expand Down Expand Up @@ -90,12 +89,6 @@ func WithDockerClient(cli DockerClient) Option {
}
}

func WithPlatform(platform string) Option {
return func(b *Builder) {
b.platform = platform
}
}

// NewBuilder creates a new instance of a Builder with static defaults.
func NewBuilder(options ...Option) *Builder {
b := &Builder{name: DefaultName}
Expand All @@ -105,22 +98,35 @@ func NewBuilder(options ...Option) *Builder {
return b
}

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
func (b *Builder) Build(ctx context.Context, f fn.Function, oo ...fn.BuildOption) (err error) {
options := fn.NewBuildOptions(oo...)
if len(options.Platforms) != 0 {
return errors.New("the pack builder does not support specifying target platforms directly.")
}

// Builder image from the function if defined, default otherwise.
builderImage, err := BuilderImage(f, b.name)
if err != nil {
return
}

if b.platform != "" {
builderImage, err = docker.GetPlatformImage(builderImage, b.platform)
// Allow for specifying a single target platform
if len(options.Platforms) > 1 {
return errors.New("the S2I builder currently only supports specifying a single target platform")
}
if len(options.Platforms) == 1 {
platform := strings.ToLower(
options.Platforms[0].OS + "/" +
options.Platforms[0].Architecture)

builderImage, err = docker.GetPlatformImage(builderImage, platform)
if err != nil {
return fmt.Errorf("cannot get platform specific image reference: %w", err)
return fmt.Errorf("cannot get platform image reference for %q: %w", platform, err)
}
}

// 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
Expand Down
4 changes: 3 additions & 1 deletion pkg/docker/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func NewRunner(verbose bool, out, errOut io.Writer) *Runner {
}

// Run the function.
func (n *Runner) Run(ctx context.Context, f fn.Function) (job *fn.Job, err error) {
func (n *Runner) Run(ctx context.Context, f fn.Function, _ ...fn.RunOption) (job *fn.Job, err error) {

var (
port = choosePort(DefaultHost, DefaultPort, DefaultDialTimeout)
Expand Down Expand Up @@ -102,6 +102,8 @@ func (n *Runner) Run(ctx context.Context, f fn.Function) (job *fn.Job, err error
}()

// Start
// TODO: Could we use the RunOption Timeout to exit if not started within
// that time?
if err = c.ContainerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
return job, errors.Wrap(err, "runner unable to start container")
}
Expand Down
103 changes: 86 additions & 17 deletions pkg/functions/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ const (
// one implementation of each supported function signature. Currently that
// includes an HTTP Handler ("http") and Cloud Events handler ("events")
DefaultTemplate = "http"

// DefaultRunTimeout when running a function which does not define its own
// timeout.
DefaultRunTimeout = 60 * time.Second
)

var (
// DefaultPlatforms is a suggestion to builder implementations which
// platforms should be the default. Due to spotty implementation support
// use of this set is left up to the discretion of the builders
// themselves. In the event the builder receives build options which
// specify a set of platforms to use in leau of the default (see the
// BuildWithPlatforms functionl option), the builder should return
// an error if the request can not proceed.
DefaultPlatforms = []Platform{
{OS: "linux", Architecture: "amd64"},
{OS: "linux", Architecture: "arm64"},
{OS: "linux", Architecture: "arm", Variant: "v7"}, // eg. RPiv4
}
)

// Client for managing function instances.
Expand All @@ -53,21 +72,20 @@ type Client struct {
instances *InstanceRefs // Function Instances management
transport http.RoundTripper // Customizable internal transport
pipelinesProvider PipelinesProvider // CI/CD pipelines management
runTimeout time.Duration // defaut timeout when running
}

// ErrNotBuilt indicates the function has not yet been built.
var ErrNotBuilt = errors.New("not built")

// ErrNameRequired indicates the operation requires a name to complete.
var ErrNameRequired = errors.New("name required")

// ErrRegistryRequired indicates the operation requires a registry to complete.
var ErrRegistryRequired = errors.New("registry required to build function, please set with `--registry` or the FUNC_REGISTRY environment variable")

// Builder of function source to runnable image.
type Builder interface {
// Build a function project with source located at path.
Build(context.Context, Function) error
Build(context.Context, Function, ...BuildOption) error
}

// Platform upon which a function may run
type Platform struct {
OS string
Architecture string
Variant string
}

// Pusher of function image to a registry.
Expand Down Expand Up @@ -103,7 +121,7 @@ type Runner interface {
// Run the function, returning a Job with metadata, error channels, and
// a stop function.The process can be stopped by running the returned stop
// function, either on context cancellation or in a defer.
Run(context.Context, Function) (*Job, error)
Run(context.Context, Function, ...RunOption) (*Job, error)
}

// Remover of deployed services.
Expand Down Expand Up @@ -206,6 +224,7 @@ func New(options ...Option) *Client {
progressListener: &NoopProgressListener{},
pipelinesProvider: &noopPipelinesProvider{},
transport: http.DefaultTransport,
runTimeout: DefaultRunTimeout,
}
c.runner = newDefaultRunner(c, os.Stdout, os.Stderr)
for _, o := range options {
Expand Down Expand Up @@ -357,6 +376,19 @@ func WithPipelinesProvider(pp PipelinesProvider) Option {
}
}

// WithRunTimeout sets a custom default timeout for functions which do not
// define their own. This is useful in situations where the client is
// operating in a restricted environment and all functions tend to take longer
// to start up than usual, or when the client is running functions which
// in general take longer to start. If a timeout is specified on the
// function itself, that will take precidence. Use the RunWithTimeout option
// on the Run method to specify a timeout with precidence.
func WithRunTimeout(t time.Duration) Option {
return func(c *Client) {
c.runTimeout = t
}
}

// ACCESSORS
// ---------

Expand Down Expand Up @@ -433,7 +465,7 @@ func (c *Client) Apply(ctx context.Context, f Function) (string, Function, error
// Returns final primary route to the Function and any errors.
func (c *Client) Update(ctx context.Context, f Function) (string, Function, error) {
if !f.Initialized() {
return "", f, ErrNotInitialized
return "", f, ErrNotInitialized{f.Root}
}
var err error
if f, err = c.Build(ctx, f); err != nil {
Expand Down Expand Up @@ -580,9 +612,34 @@ func (c *Client) Init(cfg Function) (Function, error) {
return NewFunction(oldRoot)
}

type BuildOptions struct {
Platforms []Platform
}

type BuildOption func(c *BuildOptions)

func NewBuildOptions(options ...BuildOption) BuildOptions {
oo := BuildOptions{}
for _, o := range options {
o(&oo)
}
// Note this returns the exact build options requested. It is up to the
// builder implementations to choose how to use this information.
// For example, some may error stating that building for speific platforms
// is not supported (eg pack builder). Others may use them if provided and use
// DefaultPlatforms if not (eg host builder).
return oo
}

func BuildWithPlatforms(pp []Platform) BuildOption {
return func(c *BuildOptions) {
c.Platforms = pp
}
}

// Build the function at path. Errors if the function is either unloadable or does
// not contain a populated Image.
func (c *Client) Build(ctx context.Context, f Function) (Function, error) {
func (c *Client) Build(ctx context.Context, f Function, options ...BuildOption) (Function, error) {
c.progressListener.Increment("Building function image")
ctx, cancel := context.WithCancel(ctx)
defer cancel()
Expand All @@ -609,7 +666,7 @@ func (c *Client) Build(ctx context.Context, f Function) (Function, error) {
}
}

if err = c.builder.Build(ctx, f); err != nil {
if err = c.builder.Build(ctx, f, options...); err != nil {
return f, err
}

Expand Down Expand Up @@ -837,9 +894,21 @@ func (c *Client) Route(ctx context.Context, f Function) (string, Function, error
return instance.Route, f, nil
}

type RunOptions struct {
Timeout time.Duration
}

type RunOption func(c *RunOptions)

func RunWithStartTimeout(t time.Duration) RunOption {
return func(c *RunOptions) {
c.Timeout = t
}
}

// Run the function whose code resides at root.
// On start, the chosen port is sent to the provided started channel
func (c *Client) Run(ctx context.Context, f Function) (job *Job, err error) {
func (c *Client) Run(ctx context.Context, f Function, options ...RunOption) (job *Job, err error) {
go func() {
<-ctx.Done()
c.progressListener.Stopping()
Expand All @@ -851,7 +920,7 @@ func (c *Client) Run(ctx context.Context, f Function) (job *Job, err error) {

// Run the function, which returns a Job for use interacting (at arms length)
// with that running task (which is likely inside a container process).
if job, err = c.runner.Run(ctx, f); err != nil {
if job, err = c.runner.Run(ctx, f, options...); err != nil {
return
}

Expand Down Expand Up @@ -1169,7 +1238,7 @@ func hasInitializedFunction(path string) (bool, error) {
// Builder
type noopBuilder struct{ output io.Writer }

func (n *noopBuilder) Build(ctx context.Context, _ Function) error { return nil }
func (n *noopBuilder) Build(ctx context.Context, _ Function, _ ...BuildOption) error { return nil }

// Pusher
type noopPusher struct{ output io.Writer }
Expand Down
Loading

0 comments on commit 22b077a

Please sign in to comment.