feat: enable host builder via cli (#1748)

This commit is contained in:
Luke Kingland 2023-06-21 02:33:34 +09:00 committed by GitHub
parent 94582efa49
commit 51cb15b78a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 146 additions and 107 deletions

View File

@ -14,6 +14,7 @@ import (
"knative.dev/func/pkg/builders/s2i" "knative.dev/func/pkg/builders/s2i"
"knative.dev/func/pkg/config" "knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions" fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/oci"
) )
func NewBuildCmd(newClient ClientFactory) *cobra.Command { func NewBuildCmd(newClient ClientFactory) *cobra.Command {
@ -156,8 +157,9 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro
// TODO: this logic is duplicated with runDeploy. Shouild be in buildConfig // TODO: this logic is duplicated with runDeploy. Shouild be in buildConfig
// constructor. // constructor.
// Checks if there is a difference between defined registry and its value used as a prefix in the image tag // Checks if there is a difference between defined registry and its value
// In case of a mismatch a new image tag is created and used for build // used as a prefix in the image tag In case of a mismatch a new image tag is
// created and used for build.
// Do not react if image tag has been changed outside configuration // Do not react if image tag has been changed outside configuration
if f.Registry != "" && !cmd.Flags().Changed("image") && strings.Index(f.Image, "/") > 0 && !strings.HasPrefix(f.Image, f.Registry) { if f.Registry != "" && !cmd.Flags().Changed("image") && strings.Index(f.Image, "/") > 0 && !strings.HasPrefix(f.Image, f.Registry) {
prfx := f.Registry prfx := f.Registry
@ -171,23 +173,14 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro
} }
// Client // Client
// Concrete implementations (ex builder) vary based on final effective config clientOptions, err := cfg.clientOptions()
o := []fn.Option{fn.WithRegistry(cfg.Registry)} if err != nil {
if f.Build.Builder == builders.Pack { return
o = append(o, fn.WithBuilder(pack.NewBuilder(
pack.WithName(builders.Pack),
pack.WithTimestamp(cfg.WithTimestamp),
pack.WithVerbose(cfg.Verbose))))
} else if f.Build.Builder == builders.S2I {
o = append(o, fn.WithBuilder(s2i.NewBuilder(
s2i.WithName(builders.S2I),
s2i.WithVerbose(cfg.Verbose))))
} }
client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, clientOptions...)
client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, o...)
defer done() defer done()
// Build and (optionally) push // Build
buildOptions, err := cfg.buildOptions() buildOptions, err := cfg.buildOptions()
if err != nil { if err != nil {
return return
@ -236,26 +229,6 @@ type buildConfig struct {
WithTimestamp bool 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. // newBuildConfig gathers options into a single build request.
func newBuildConfig() buildConfig { func newBuildConfig() buildConfig {
return buildConfig{ return buildConfig{
@ -369,3 +342,62 @@ func (c buildConfig) Validate() (err error) {
return return
} }
// clientOptions returns options suitable for instantiating a client based on
// the current state of the build config object.
// This will be unnecessary and refactored away when the host-based OCI
// builder and pusher are the default implementations and the Pack and S2I
// constructors simplified.
//
// TODO: Platform is currently only used by the S2I builder. This should be
// a multi-valued argument which passes through to the "host" builder (which
// supports multi-arch/platform images), and throw an error if either trying
// to specify a platform for buildpacks, or trying to specify more than one
// for S2I.
//
// TODO: As a further optimization, it might be ideal to only build the
// image necessary for the target cluster, since the end product of a function
// deployment is not the contiainer, but rather the running service.
func (c buildConfig) clientOptions() ([]fn.Option, error) {
o := []fn.Option{fn.WithRegistry(c.Registry)}
if c.Builder == builders.Host {
o = append(o,
fn.WithBuilder(oci.NewBuilder(builders.Host, c.Verbose)),
fn.WithPusher(oci.NewPusher(false, c.Verbose)))
} else if c.Builder == builders.Pack {
o = append(o,
fn.WithBuilder(pack.NewBuilder(
pack.WithName(builders.Pack),
pack.WithTimestamp(c.WithTimestamp),
pack.WithVerbose(c.Verbose))))
} else if c.Builder == builders.S2I {
o = append(o,
fn.WithBuilder(s2i.NewBuilder(
s2i.WithName(builders.S2I),
s2i.WithVerbose(c.Verbose))))
} else {
return o, builders.ErrUnknownBuilder{Name: c.Builder, Known: KnownBuilders()}
}
return o, nil
}
// buildOptions returns options for use with the client.Build request
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
}

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os"
"strconv" "strconv"
"strings" "strings"
@ -13,8 +14,6 @@ import (
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
"knative.dev/client-pkg/pkg/util" "knative.dev/client-pkg/pkg/util"
"knative.dev/func/pkg/builders" "knative.dev/func/pkg/builders"
"knative.dev/func/pkg/builders/buildpacks"
"knative.dev/func/pkg/builders/s2i"
"knative.dev/func/pkg/config" "knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions" fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/k8s" "knative.dev/func/pkg/k8s"
@ -251,26 +250,11 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) {
// Informative non-error messages regarding the final deployment request // Informative non-error messages regarding the final deployment request
printDeployMessages(cmd.OutOrStdout(), cfg) printDeployMessages(cmd.OutOrStdout(), cfg)
// Client clientOptions, err := cfg.clientOptions()
// Concrete implementations (ex builder) vary based on final effective cfg. if err != nil {
var builder fn.Builder return
if f.Build.Builder == builders.Pack {
builder = buildpacks.NewBuilder(
buildpacks.WithName(builders.Pack),
buildpacks.WithVerbose(cfg.Verbose),
buildpacks.WithTimestamp(cfg.Timestamp),
)
} else if f.Build.Builder == builders.S2I {
builder = s2i.NewBuilder(
s2i.WithName(builders.S2I),
s2i.WithVerbose(cfg.Verbose))
} else {
return builders.ErrUnknownBuilder{Name: f.Build.Builder, Known: KnownBuilders()}
} }
client, done := newClient(ClientConfig{Namespace: f.Deploy.Namespace, Verbose: cfg.Verbose}, clientOptions...)
client, done := newClient(ClientConfig{Namespace: f.Deploy.Namespace, Verbose: cfg.Verbose},
fn.WithRegistry(cfg.Registry),
fn.WithBuilder(builder))
defer done() defer done()
// Deploy // Deploy
@ -281,15 +265,12 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) {
return return
} }
} else { } else {
if shouldBuild(cfg.Build, f, client) { // --build or "auto" with FS changes var buildOptions []fn.BuildOption
buildOptions, err := cfg.buildOptions() if buildOptions, err = cfg.buildOptions(); err != nil {
if err != nil { return
return err }
} if f, err = build(cmd, cfg.Build, f, client, buildOptions); err != nil {
return
if f, err = client.Build(cmd.Context(), f, buildOptions...); err != nil {
return err
}
} }
if cfg.Push { if cfg.Push {
if f, err = client.Push(cmd.Context(), f); err != nil { if f, err = client.Push(cmd.Context(), f); err != nil {
@ -313,16 +294,30 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) {
return f.Stamp() return f.Stamp()
} }
// shouldBuild returns true if the value of the build option is a truthy value, // build when flag == 'auto' and the function is out-of-date, or when the
// or if it is the literal "auto" and the function reports as being currently // flag value is explicitly truthy such as 'true' or '1'. Error if flag
// unbuilt. Invalid errors are not reported as this is the purview of // is neither 'auto' nor parseable as a boolean. Return CLI-specific error
// deployConfig.Validate // message verbeage suitable for both Deploy and Run commands which feature an
func shouldBuild(buildCfg string, f fn.Function, client *fn.Client) bool { // optional build step.
if buildCfg == "auto" { func build(cmd *cobra.Command, flag string, f fn.Function, client *fn.Client, buildOptions []fn.BuildOption) (fn.Function, error) {
return !f.Built() // first build or modified filesystem var err error
if flag == "auto" {
if f.Built() {
fmt.Fprintln(cmd.OutOrStdout(), "function up-to-date. Force rebuild with --build")
} else {
if f, err = client.Build(cmd.Context(), f, buildOptions...); err != nil {
return f, err
}
}
} else if build, _ := strconv.ParseBool(flag); build {
if f, err = client.Build(cmd.Context(), f, buildOptions...); err != nil {
return f, err
}
} else if _, err = strconv.ParseBool(flag); err != nil {
return f, fmt.Errorf("--build ($FUNC_BUILD) %q not recognized. Should be 'auto' or a truthy value such as 'true', 'false', '0', or '1'.", flag)
} }
build, _ := strconv.ParseBool(buildCfg) return f, nil
return build
} }
func NewRegistryValidator(path string) survey.Validator { func NewRegistryValidator(path string) survey.Validator {
@ -368,6 +363,19 @@ func KnownBuilders() builders.Known {
// the set of builders enumerated in the builders pacakage. // the set of builders enumerated in the builders pacakage.
// However, future third-party integrations may support less than, or more // However, future third-party integrations may support less than, or more
// builders, and certain environmental considerations may alter this list. // builders, and certain environmental considerations may alter this list.
// Also a good place to stick feature-flags; to wit:
enable_host, _ := strconv.ParseBool(os.Getenv("FUNC_ENABLE_HOST_BUILDER"))
if !enable_host {
bb := []string{}
for _, b := range builders.All() {
if b != builders.Host {
bb = append(bb, b)
}
}
return bb
}
return builders.All() return builders.All()
} }

View File

@ -1516,9 +1516,9 @@ func TestDeploy_UnsetFlag(t *testing.T) {
} }
// Test_ValidateBuilder tests that the bulder validation accepts the // Test_ValidateBuilder tests that the bulder validation accepts the
// accepts === the set of known builders. // the set of known builders, and spot-checks an error is thrown for unknown.
func Test_ValidateBuilder(t *testing.T) { func Test_ValidateBuilder(t *testing.T) {
for _, name := range builders.All() { for _, name := range KnownBuilders() {
if err := ValidateBuilder(name); err != nil { if err := ValidateBuilder(name); err != nil {
t.Fatalf("expected builder '%v' to be valid, but got error: %v", name, err) t.Fatalf("expected builder '%v' to be valid, but got error: %v", name, err)
} }

View File

@ -47,10 +47,10 @@ func TestInvoke(t *testing.T) {
fmt.Fprintf(os.Stderr, "error serving: %v", err) fmt.Fprintf(os.Stderr, "error serving: %v", err)
} }
}() }()
_, port, _ := net.SplitHostPort(l.Addr().String()) host, port, _ := net.SplitHostPort(l.Addr().String())
errs := make(chan error, 10) errs := make(chan error, 10)
stop := func() error { _ = s.Close(); return nil } stop := func() error { _ = s.Close(); return nil }
return fn.NewJob(f, "127.0.0.1", port, errs, stop, false) return fn.NewJob(f, host, port, errs, stop, false)
} }
// Run the mock http service function interloper // Run the mock http service function interloper

View File

@ -12,9 +12,6 @@ import (
"github.com/ory/viper" "github.com/ory/viper"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"knative.dev/func/pkg/builders"
pack "knative.dev/func/pkg/builders/buildpacks"
"knative.dev/func/pkg/builders/s2i"
"knative.dev/func/pkg/config" "knative.dev/func/pkg/config"
"knative.dev/func/pkg/docker" "knative.dev/func/pkg/docker"
fn "knative.dev/func/pkg/functions" fn "knative.dev/func/pkg/functions"
@ -148,10 +145,10 @@ func runRun(cmd *cobra.Command, args []string, newClient ClientFactory) (err err
if cfg, err = newRunConfig(cmd).Prompt(); err != nil { if cfg, err = newRunConfig(cmd).Prompt(); err != nil {
return return
} }
if err = cfg.Validate(cmd); err != nil { if f, err = fn.NewFunction(cfg.Path); err != nil {
return return
} }
if f, err = fn.NewFunction(cfg.Path); err != nil { if err = cfg.Validate(cmd, f); err != nil {
return return
} }
if !f.Initialized() { if !f.Initialized() {
@ -176,39 +173,30 @@ func runRun(cmd *cobra.Command, args []string, newClient ClientFactory) (err err
} }
// Client // Client
// clientOptions, err := cfg.clientOptions()
// Builder and runner implementations are based on the value of f.Build.Builder, and if err != nil {
// return
o := []fn.Option{}
if f.Build.Builder == builders.Pack {
o = append(o, fn.WithBuilder(pack.NewBuilder(
pack.WithName(builders.Pack),
pack.WithVerbose(cfg.Verbose))))
} else if f.Build.Builder == builders.S2I {
o = append(o, fn.WithBuilder(s2i.NewBuilder(
s2i.WithName(builders.S2I),
s2i.WithVerbose(cfg.Verbose))))
} }
if cfg.Container { if cfg.Container {
o = append(o, fn.WithRunner(docker.NewRunner(cfg.Verbose, os.Stdout, os.Stderr))) clientOptions = append(clientOptions, fn.WithRunner(docker.NewRunner(cfg.Verbose, os.Stdout, os.Stderr)))
} }
if cfg.StartTimeout != 0 { if cfg.StartTimeout != 0 {
o = append(o, fn.WithStartTimeout(cfg.StartTimeout)) clientOptions = append(clientOptions, fn.WithStartTimeout(cfg.StartTimeout))
} }
client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, o...) client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, clientOptions...)
defer done() defer done()
// Build // Build
// //
// If requesting to run via the container, build the container if it is // If requesting to run via the container, build the container if it is
// either out-of-date or a build was explicitly requested. // either out-of-date or a build was explicitly requested.
if cfg.Container && shouldBuild(cfg.Build, f, client) { if cfg.Container {
buildOptions, err := cfg.buildOptions() buildOptions, err := cfg.buildOptions()
if err != nil { if err != nil {
return err return err
} }
if f, err = client.Build(cmd.Context(), f, buildOptions...); err != nil { if f, err = build(cmd, cfg.Build, f, client, buildOptions); err != nil {
return err return err
} }
} }
@ -321,7 +309,7 @@ func (c runConfig) Prompt() (runConfig, error) {
return c, nil return c, nil
} }
func (c runConfig) Validate(cmd *cobra.Command) (err error) { func (c runConfig) Validate(cmd *cobra.Command, f fn.Function) (err error) {
// Bubble // Bubble
if err = c.buildConfig.Validate(); err != nil { if err = c.buildConfig.Validate(); err != nil {
return return
@ -335,13 +323,13 @@ func (c runConfig) Validate(cmd *cobra.Command) (err error) {
} }
// There is currently no local host runner implemented, so specifying // There is currently no local host runner implemented, so specifying
// --container=false should always return an informative error to the user // --container=false should return an informative error for runtimes other
// such that they do not receive the rather cryptic "no runner defined" // than Go that is more helpful than the cryptic, though correct, error
// error from a Client instance which was instantiated with no runner. // from the Client that it was instantated without a runner.
// TODO: modify this check when the local host runner is available to // TODO: modify this check when the local host runner is available to
// only generate this error when --container==false && the --language is // only generate this error when --container==false && the --language is
// not yet implemented. // not yet implemented.
if !c.Container { if !c.Container && f.Runtime != "go" {
return errors.New("the ability to run functions outside of a container via 'func run' is coming soon.") return errors.New("the ability to run functions outside of a container via 'func run' is coming soon.")
} }

View File

@ -13,6 +13,7 @@ import (
) )
const ( const (
Host = "host"
Pack = "pack" Pack = "pack"
S2I = "s2i" S2I = "s2i"
Default = Pack Default = Pack
@ -22,7 +23,7 @@ const (
type Known []string type Known []string
func All() Known { func All() Known {
return Known([]string{Pack, S2I}) return Known([]string{Host, Pack, S2I})
} }
func (k Known) String() string { func (k Known) String() string {

View File

@ -1,3 +1,6 @@
//go:build !integration
// +build !integration
package scaffolding package scaffolding
import ( import (

View File

@ -0,0 +1,7 @@
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}