Skip to content

Commit

Permalink
builder short name constants etc throughout (#1180)
Browse files Browse the repository at this point in the history
* chore: shared builder constants etc

* pretty-print the shared unknown builder error

* update builder impls to use shared defs and validators

* error and docs text formatting

* include static default short names

* comment updates and typos

* docs paths

* use the constants for the in-package builder defaults

* use builders.All but with caviat
  • Loading branch information
lkingland committed Aug 24, 2022
1 parent b3ced5e commit e6ec11b
Show file tree
Hide file tree
Showing 19 changed files with 335 additions and 241 deletions.
97 changes: 97 additions & 0 deletions builders/builders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
Package builders provides constants for builder implementation short names,
shared error types and convienience functions.
*/
package builders

import (
"fmt"
"strconv"
"strings"

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

const (
Pack = "pack"
S2I = "s2i"
Default = Pack
)

// Known builder names with a pretty-printed string representation
type Known []string

func All() Known {
return Known([]string{Pack, S2I})
}

func (k Known) String() string {
var b strings.Builder
for i, v := range k {
if i < len(k)-2 {
b.WriteString(strconv.Quote(v) + ", ")
} else if i < len(k)-1 {
b.WriteString(strconv.Quote(v) + " and ")
} else {
b.WriteString(strconv.Quote(v))
}
}
return b.String()
}

// ErrUnknownBuilder may be used by whomever is choosing a concrete
// implementation of a builder to invoke based on potentially invalid input.
type ErrUnknownBuilder struct {
Name string
Known Known
}

func (e ErrUnknownBuilder) Error() string {
if len(e.Known) == 0 {
return fmt.Sprintf("\"%v\" is not a known builder", e.Name)
}
if len(e.Known) == 1 {
return fmt.Sprintf("\"%v\" is not a known builder. The available builder is %v", e.Name, e.Known)
}
return fmt.Sprintf("\"%v\" is not a known builder. Available builders are %s", e.Name, e.Known)
}

// ErrRuntimeRequired
type ErrRuntimeRequired struct {
Builder string
}

func (e ErrRuntimeRequired) Error() string {
return fmt.Sprintf("runtime required to choose a default '%v' builder image", e.Builder)
}

// ErrNoDefaultImage
type ErrNoDefaultImage struct {
Builder string
Runtime string
}

func (e ErrNoDefaultImage) Error() string {
return fmt.Sprintf("the '%v' runtime defines no default '%v' builder image", e.Runtime, e.Builder)
}

// Image is a convenience function for choosing the correct builder image
// given a function, a builder, and defaults grouped by runtime.
// - ErrRuntimeRequired if no runtime was provided on the given function
// - ErrNoDefaultImage if the function has no builder image already defined
// for the given runtieme and there is no default in the provided map.
func Image(f fn.Function, builder string, defaults map[string]string) (string, error) {
v, ok := f.BuilderImages[builder]
if ok {
return v, nil // found value
}
if f.Runtime == "" {
return "", ErrRuntimeRequired{Builder: builder}
}
v, ok = defaults[f.Runtime]
if ok {
return v, nil // Found default
}
return "", ErrNoDefaultImage{Builder: builder, Runtime: f.Runtime}

}
95 changes: 95 additions & 0 deletions builders/builders_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package builders_test

import (
"errors"
"testing"

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

// TestImage_Named ensures that a builder image is returned when
// it exists on the function for a given builder, no defaults.
func TestImage_Named(t *testing.T) {
f := fn.Function{
Builder: builders.Pack,
BuilderImages: map[string]string{
builders.Pack: "example.com/my/builder-image",
},
}

builderImage, err := builders.Image(f, builders.Pack, make(map[string]string))
if err != nil {
t.Fatal(err)
}
if builderImage != "example.com/my/builder-image" {
t.Fatalf("expected 'example.com/my/builder-image', got '%v'", builderImage)
}
}

// TestImage_ErrRuntimeRequired ensures that the correct error is thrown when
// the function has no builder image yet defined for the named builder, and
// also no runtime to choose from the defaults.
func TestImage_ErrRuntimeRequired(t *testing.T) {
_, err := builders.Image(fn.Function{}, "", make(map[string]string))
if err == nil {
t.Fatalf("did not receive expected error")
}
if !errors.Is(err, builders.ErrRuntimeRequired{}) {
t.Fatalf("error is not an 'ErrRuntimeRequired': '%v'", err)
}
}

// TestImage_ErrNoDefaultImage ensures that when
func TestImage_ErrNoDefaultImage(t *testing.T) {
_, err := builders.Image(fn.Function{Runtime: "go"}, "", make(map[string]string))
if err == nil {
t.Fatalf("did not receive expected error")
}
if !errors.Is(err, builders.ErrNoDefaultImage{Runtime: "go"}) {
t.Fatalf("did not get 'ErrNoDefaultImage', got '%v'", err)
}
}

// TestImage_Defaults ensures that, when a default exists in the provided
// map, it is chosen when both runtime is defined on the function and no
// builder image has yet to be defined on the function.
func TestImage_Defaults(t *testing.T) {
defaults := map[string]string{
"go": "example.com/go/default-builder-image",
}
builderImage, err := builders.Image(fn.Function{Runtime: "go"}, "", defaults)
if err != nil {
t.Fatal(err)
}

if builderImage != "example.com/go/default-builder-image" {
t.Fatalf("the default was not chosen")
}
}

// Test_ErrUnknownBuilder ensures that the error properfly formats.
// This error is used externally by packages which share builders but may
// define their own custom builder, thus actually throwing this error
// is the responsibility of whomever is instantiating builders.
func Test_ErrUnknownBuilder(t *testing.T) {
var tests = []struct {
Known []string
Expected string
}{
{[]string{},
`"test" is not a known builder`},
{[]string{"pack"},
`"test" is not a known builder. The available builder is "pack"`},
{[]string{"pack", "s2i"},
`"test" is not a known builder. Available builders are "pack" and "s2i"`},
{[]string{"pack", "s2i", "custom"},
`"test" is not a known builder. Available builders are "pack", "s2i" and "custom"`},
}
for _, test := range tests {
e := builders.ErrUnknownBuilder{Name: "test", Known: test.Known}
if e.Error() != test.Expected {
t.Fatalf("expected error \"%v\". got \"%v\"", test.Expected, e.Error())
}
}
}
48 changes: 17 additions & 31 deletions buildpacks/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ import (
"github.com/docker/docker/client"

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

// DefaultName when no WithName option is provided to NewBuilder
const DefaultName = builders.Pack

var (
DefaultBuilderImages = map[string]string{
"node": "gcr.io/paketo-buildpacks/builder:base",
Expand All @@ -40,6 +44,7 @@ var (

// Builder will build Function using Pack.
type Builder struct {
name string
verbose bool
logger io.Writer
impl Impl
Expand All @@ -52,7 +57,7 @@ type Impl interface {

// NewBuilder instantiates a Buildpack-based Builder
func NewBuilder(options ...Option) *Builder {
b := &Builder{}
b := &Builder{name: DefaultName}
for _, o := range options {
o(b)
}
Expand All @@ -69,6 +74,12 @@ func NewBuilder(options ...Option) *Builder {

type Option func(*Builder)

func WithName(n string) Option {
return func(b *Builder) {
b.name = n
}
}

func WithVerbose(v bool) Option {
return func(b *Builder) {
b.verbose = v
Expand All @@ -83,8 +94,8 @@ func WithImpl(i Impl) Option {

// Build the Function at path.
func (b *Builder) Build(ctx context.Context, f fn.Function) (err error) {
// Builder image defined on the Function if set, or from the default map.
image, err := BuilderImage(f)
// Builder image from the function if defined, default otherwise.
image, err := BuilderImage(f, b.name)
if err != nil {
return
}
Expand Down Expand Up @@ -165,34 +176,9 @@ func newImpl(ctx context.Context, cli client.CommonAPIClient, dockerHost string,
return pack.NewClient(pack.WithLogger(logging.NewSimpleLogger(logger)), pack.WithDockerClient(cli))
}

// Builder Image
//
// A value defined on the Function itself takes precidence. If not defined,
// the default builder image for the Function's language runtime is used.
// An inability to determine a builder image (such as an unknown language),
// will return empty string. Errors are returned if either the runtime is not
// populated or an inability to locate a default.
//
// Exported for use by Tekton in-cluster builds which do not have access to this
// library at this time, and can therefore not instantiate and invoke this
// package's buildpacks.Builder.Build. Instead, they must transmit information
// to the cluster using a Pipeline definition.
func BuilderImage(f fn.Function) (string, error) {
if f.Runtime == "" {
return "", ErrRuntimeRequired{}
}

v, ok := f.BuilderImages[fn.BuilderPack]
if ok {
return v, nil
}

v, ok = DefaultBuilderImages[f.Runtime]
if ok {
return v, nil
}

return "", ErrRuntimeNotSupported{f.Runtime}
// Builder Image chooses the correct builder image or defaults.
func BuilderImage(f fn.Function, builderName string) (string, error) {
return builders.Image(f, builderName, DefaultBuilderImages)
}

// podmanPreV330 returns if the daemon is podman pre v330 or errors trying.
Expand Down
39 changes: 9 additions & 30 deletions buildpacks/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,17 @@ package buildpacks

import (
"context"
"errors"
"testing"

pack "github.com/buildpacks/pack/pkg/client"
fn "knative.dev/kn-plugin-func"
"knative.dev/kn-plugin-func/builders"
. "knative.dev/kn-plugin-func/testing"
)

// Test_ErrRuntimeRequired ensures that a request to build without a runtime
// defined for the Function yields an ErrRuntimeRequired
func Test_ErrRuntimeRequired(t *testing.T) {
b := NewBuilder()
err := b.Build(context.Background(), fn.Function{})

if !errors.As(err, &ErrRuntimeRequired{}) {
t.Fatalf("expected ErrRuntimeRequired not received. Got %v", err)
}
}

// Test_ErrRuntimeNotSupported ensures that a request to build a function whose
// runtime is not yet supported yields an ErrRuntimeNotSupported
func Test_ErrRuntimeNotSupported(t *testing.T) {
b := NewBuilder()
err := b.Build(context.Background(), fn.Function{Runtime: "unsupported"})

if !errors.As(err, &ErrRuntimeNotSupported{}) {
t.Fatalf("expected ErrRuntimeNotSupported not received. got %v", err)
}
}

// Test_ImageDefault ensures that a Function bing built which does not
// Test_BuilderImageDefault ensures that a Function bing built which does not
// define a Builder Image will get the internally-defined default.
func Test_ImageDefault(t *testing.T) {
func Test_BuilderImageDefault(t *testing.T) {
var (
i = &mockImpl{}
b = NewBuilder(WithImpl(i))
Expand All @@ -59,18 +37,19 @@ func Test_ImageDefault(t *testing.T) {
// image defined on the given Function if provided.
func Test_BuilderImageConfigurable(t *testing.T) {
var (
i = &mockImpl{} // mock underlying implementation
b = NewBuilder(WithImpl(i)) // Func Builder logic
f = fn.Function{ // Function with a builder image set
i = &mockImpl{} // mock underlying implementation
b = NewBuilder( // Func Builder logic
WithName(builders.Pack), WithImpl(i))
f = fn.Function{ // Function with a builder image set
Runtime: "node",
BuilderImages: map[string]string{
"pack": "example.com/user/builder-image",
builders.Pack: "example.com/user/builder-image",
},
}
)

i.BuildFn = func(ctx context.Context, opts pack.BuildOptions) error {
expected := f.BuilderImages["pack"]
expected := "example.com/user/builder-image"
if opts.Builder != expected {
t.Fatalf("expected builder image for node to be '%v', got '%v'", expected, opts.Builder)
}
Expand Down
5 changes: 3 additions & 2 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

cloudevents "github.com/cloudevents/sdk-go/v2"
fn "knative.dev/kn-plugin-func"
"knative.dev/kn-plugin-func/builders"
"knative.dev/kn-plugin-func/mock"
. "knative.dev/kn-plugin-func/testing"
)
Expand Down Expand Up @@ -943,8 +944,8 @@ func TestClient_New_BuildersPersisted(t *testing.T) {
Runtime: TestRuntime,
Root: root,
BuilderImages: map[string]string{
"pack": "example.com/my/custom-pack-builder",
"s2i": "example.com/my/custom-s2i-builder",
builders.Pack: "example.com/my/custom-pack-builder",
builders.S2I: "example.com/my/custom-s2i-builder",
}}

// Create the function, which should preserve custom builders
Expand Down
Loading

0 comments on commit e6ec11b

Please sign in to comment.