base builder flag (#2935)

This commit is contained in:
David Fridrich 2025-07-11 21:58:06 +02:00 committed by GitHub
parent 30315ea15e
commit 211df1657f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 215 additions and 14 deletions

View File

@ -69,8 +69,8 @@ EXAMPLES
`, `,
SuggestFor: []string{"biuld", "buidl", "built"}, SuggestFor: []string{"biuld", "buidl", "built"},
PreRunE: bindEnv("image", "path", "builder", "registry", "confirm", PreRunE: bindEnv("image", "path", "builder", "registry", "confirm",
"push", "builder-image", "platform", "verbose", "build-timestamp", "push", "builder-image", "base-image", "platform", "verbose",
"registry-insecure", "username", "password", "token"), "build-timestamp", "registry-insecure", "username", "password", "token"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runBuild(cmd, args, newClient) return runBuild(cmd, args, newClient)
}, },
@ -110,6 +110,8 @@ EXAMPLES
builderImage := f.Build.BuilderImages[f.Build.Builder] builderImage := f.Build.BuilderImages[f.Build.Builder]
cmd.Flags().StringP("builder-image", "", builderImage, cmd.Flags().StringP("builder-image", "", builderImage,
"Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)") "Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)")
cmd.Flags().StringP("base-image", "", f.Build.BaseImage,
"Override the base image for your function (host builder only)")
cmd.Flags().StringP("image", "i", f.Image, cmd.Flags().StringP("image", "i", f.Image,
"Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry ($FUNC_IMAGE)") "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry ($FUNC_IMAGE)")
@ -223,6 +225,10 @@ type buildConfig struct {
// image name derivation based on registry and function name) // image name derivation based on registry and function name)
Image string Image string
// BaseImage is an image to build a function upon (host builder only)
// TODO: gauron99 -- make option to add a path to dockerfile ?
BaseImage string
// Path of the function implementation on local disk. Defaults to current // Path of the function implementation on local disk. Defaults to current
// working directory of the process. // working directory of the process.
Path string Path string
@ -260,6 +266,7 @@ func newBuildConfig() buildConfig {
RegistryInsecure: viper.GetBool("registry-insecure"), RegistryInsecure: viper.GetBool("registry-insecure"),
}, },
BuilderImage: viper.GetString("builder-image"), BuilderImage: viper.GetString("builder-image"),
BaseImage: viper.GetString("base-image"),
Image: viper.GetString("image"), Image: viper.GetString("image"),
Path: viper.GetString("path"), Path: viper.GetString("path"),
Platform: viper.GetString("platform"), Platform: viper.GetString("platform"),
@ -281,6 +288,7 @@ func (c buildConfig) Configure(f fn.Function) fn.Function {
f.Build.BuilderImages[f.Build.Builder] = c.BuilderImage f.Build.BuilderImages[f.Build.Builder] = c.BuilderImage
} }
f.Image = c.Image f.Image = c.Image
f.Build.BaseImage = c.BaseImage
// Path, Platform and Push are not part of a function's state. // Path, Platform and Push are not part of a function's state.
return f return f
} }
@ -360,6 +368,10 @@ func (c buildConfig) Validate() (err error) {
return return
} }
// BaseImage is only supported with the host builder
if c.BaseImage != "" && c.Builder != "host" {
err = errors.New("only host builds support specifying the base image")
}
return return
} }

View File

@ -95,6 +95,12 @@ func TestBuild_Authentication(t *testing.T) {
testAuthentication(NewBuildCmd, t) testAuthentication(NewBuildCmd, t)
} }
// TestBuild_BaseImage ensures that base image is used only with the right
// builders and propagates into f.Build.BaseImage
func TestBuild_BaseImage(t *testing.T) {
testBaseImage(NewBuildCmd, t)
}
// TestBuild_Push ensures that the build command properly pushes and respects // TestBuild_Push ensures that the build command properly pushes and respects
// the --push flag. // the --push flag.
// - Push triggered after a successful build // - Push triggered after a successful build

View File

@ -128,7 +128,11 @@ EXAMPLES
`, `,
SuggestFor: []string{"delpoy", "deplyo"}, SuggestFor: []string{"delpoy", "deplyo"},
PreRunE: bindEnv("build", "build-timestamp", "builder", "builder-image", "confirm", "domain", "env", "git-branch", "git-dir", "git-url", "image", "namespace", "path", "platform", "push", "pvc-size", "service-account", "registry", "registry-insecure", "remote", "username", "password", "token", "verbose", "remote-storage-class"), PreRunE: bindEnv("build", "build-timestamp", "builder", "builder-image",
"base-image", "confirm", "domain", "env", "git-branch", "git-dir",
"git-url", "image", "namespace", "path", "platform", "push", "pvc-size",
"service-account", "registry", "registry-insecure", "remote",
"username", "password", "token", "verbose", "remote-storage-class"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDeploy(cmd, newClient) return runDeploy(cmd, newClient)
}, },
@ -163,6 +167,8 @@ EXAMPLES
builderImage := f.Build.BuilderImages[f.Build.Builder] builderImage := f.Build.BuilderImages[f.Build.Builder]
cmd.Flags().String("builder-image", builderImage, cmd.Flags().String("builder-image", builderImage,
"Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)") "Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)")
cmd.Flags().StringP("base-image", "", f.Build.BaseImage,
"Override the base image for your function (host builder only)")
cmd.Flags().StringP("image", "i", f.Image, cmd.Flags().StringP("image", "i", f.Image,
"Full image name in the form [registry]/[namespace]/[name]:[tag]@[digest]. This option takes precedence over --registry. Specifying digest is optional, but if it is given, 'build' and 'push' phases are disabled. ($FUNC_IMAGE)") "Full image name in the form [registry]/[namespace]/[name]:[tag]@[digest]. This option takes precedence over --registry. Specifying digest is optional, but if it is given, 'build' and 'push' phases are disabled. ($FUNC_IMAGE)")

View File

@ -2170,3 +2170,75 @@ func Test_isDigested(t *testing.T) {
t.Fatal("did not report image reference has digest") t.Fatal("did not report image reference has digest")
} }
} }
func TestDeploy_BaseImage(t *testing.T) {
testBaseImage(NewDeployCmd, t)
}
func testBaseImage(cmdFn commandConstructor, t *testing.T) {
const baseImage = "example.com/repo/baseImage"
tests := []struct {
name string
runtime string
builder string
expErr bool
}{
{
name: "should-succeed: python-runtime with host-builder",
runtime: "python",
builder: "host",
},
{
name: "should-succeed: go-runtime with host-builder",
runtime: "go",
builder: "host",
},
{
name: "should-fail: python-runtime with pack-builder",
runtime: "python",
builder: "pack",
expErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := FromTempDirectory(t)
// func init
f := fn.Function{Runtime: tt.runtime, Root: root}
_, err := fn.New().Init(f)
if err != nil {
t.Fatal(err)
}
//create cmd
cmd := cmdFn(NewTestClient(
fn.WithBuilder(mock.NewBuilder()),
fn.WithDeployer(mock.NewDeployer()),
fn.WithRegistry(TestRegistry),
))
// create flags for cmd
args := []string{
fmt.Sprintf("--builder=%s", tt.builder),
fmt.Sprintf("--base-image=%s", baseImage),
}
cmd.SetArgs(args)
err = cmd.Execute()
// ASSERT
// got error but expected success
if err != nil && !tt.expErr {
err = fmt.Errorf("Expected the test to succeed but instead got: %w", err)
t.Fatal(err)
}
// succeeded but expected fail
if err == nil && tt.expErr {
t.Fatal(fmt.Errorf("Expected error but test succeeded"))
}
})
}
}

View File

@ -77,7 +77,9 @@ EXAMPLES
$ {{rootCmdUse}} run --json $ {{rootCmdUse}} run --json
`, `,
SuggestFor: []string{"rnu"}, SuggestFor: []string{"rnu"},
PreRunE: bindEnv("build", "builder", "builder-image", "confirm", "container", "env", "image", "path", "registry", "start-timeout", "verbose", "address", "json"), PreRunE: bindEnv("build", "builder", "builder-image", "base-image",
"confirm", "container", "env", "image", "path", "registry",
"start-timeout", "verbose", "address", "json"),
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
return runRun(cmd, newClient) return runRun(cmd, newClient)
}, },
@ -109,6 +111,8 @@ EXAMPLES
builderImage := f.Build.BuilderImages[f.Build.Builder] builderImage := f.Build.BuilderImages[f.Build.Builder]
cmd.Flags().String("builder-image", builderImage, cmd.Flags().String("builder-image", builderImage,
"Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)") "Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)")
cmd.Flags().StringP("base-image", "", f.Build.BaseImage,
"Override the base image for your function (host builder only)")
cmd.Flags().StringP("image", "i", f.Image, cmd.Flags().StringP("image", "i", f.Image,
"Full image name in the form [registry]/[namespace]/[name]:[tag]. This option takes precedence over --registry. Specifying tag is optional. ($FUNC_IMAGE)") "Full image name in the form [registry]/[namespace]/[name]:[tag]. This option takes precedence over --registry. Specifying tag is optional. ($FUNC_IMAGE)")
cmd.Flags().StringArrayP("env", "e", []string{}, cmd.Flags().StringArrayP("env", "e", []string{},

View File

@ -515,3 +515,89 @@ func TestRun_Address(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
// TestRun_BaseImage ensures that running func run --base-image with various
// other
func TestRun_BaseImage(t *testing.T) {
const baseImage = "example.com/repo/baseImage"
tests := []struct {
name string
runtime string
builder string
expectError bool
}{
{
name: "should-succeed: python-runtime with host-builder",
runtime: "python",
builder: "host",
},
{
name: "should-succeed: go-runtime with host-builder",
runtime: "go",
builder: "host",
},
{
name: "should-fail: python-runtime with pack-builder",
runtime: "python",
builder: "pack",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := FromTempDirectory(t)
runner := mock.NewRunner()
runner.RunFn = func(_ context.Context, f fn.Function, _ string, _ time.Duration) (*fn.Job, error) {
errs := make(chan error, 1)
stop := func() error { return nil }
return fn.NewJob(f, "127.0.0.1", "8080", errs, stop, false)
}
builder := mock.NewBuilder()
//if tt.expectError {
// builder.BuildFn = func(f fn.Function) error { return fmt.Errorf("expected error") }
//}
cmd := NewRunCmd(NewTestClient(
fn.WithRunner(runner),
fn.WithBuilder(builder),
fn.WithRegistry(TestRegistry),
))
args := []string{"--build=true", fmt.Sprintf("--builder=%s", tt.builder), fmt.Sprintf("--base-image=%s", baseImage)}
cmd.SetArgs(args)
// set test case's function instance
_, err := fn.New().Init(fn.Function{Root: root, Runtime: tt.runtime})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
runErrCh := make(chan error, 1)
go func() {
t0 := tt // capture tt into closure
_, err := cmd.ExecuteContextC(ctx)
if err != nil && t0.expectError {
// This is an expected error, so simply continue execution ignoring
// the error (send nil on the channel to release the parent routine
runErrCh <- nil
return
} else if err != nil {
runErrCh <- err // error not expected
return
}
// No errors, but an error was expected:
if t0.expectError {
runErrCh <- fmt.Errorf("Expected error but got '%v'\n", err)
}
close(runErrCh) // release the waiting parent process
}()
cancel() // trigger the return of cmd.ExecuteContextC in the routine
<-ctx.Done()
if err := <-runErrCh; err != nil { // wait for completion of assertions
t.Fatal(err)
}
})
}
}

View File

@ -57,6 +57,7 @@ func build
### Options ### Options
``` ```
--base-image string Override the base image for your function (host builder only)
--build-timestamp Use the actual time as the created time for the docker image. This is only useful for buildpacks builder. --build-timestamp Use the actual time as the created time for the docker image. This is only useful for buildpacks builder.
-b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". ($FUNC_BUILDER) (default "pack") -b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". ($FUNC_BUILDER) (default "pack")
--builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE) --builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)

View File

@ -113,6 +113,7 @@ func deploy
### Options ### Options
``` ```
--base-image string Override the base image for your function (host builder only)
--build string[="true"] Build the function. [auto|true|false]. ($FUNC_BUILD) (default "auto") --build string[="true"] Build the function. [auto|true|false]. ($FUNC_BUILD) (default "auto")
--build-timestamp Use the actual time as the created time for the docker image. This is only useful for buildpacks builder. --build-timestamp Use the actual time as the created time for the docker image. This is only useful for buildpacks builder.
-b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". (default "pack") -b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". (default "pack")

View File

@ -67,6 +67,7 @@ func run
``` ```
--address string Interface and port on which to bind and listen. Default is 127.0.0.1:8080, or an available port if 8080 is not available. ($FUNC_ADDRESS) --address string Interface and port on which to bind and listen. Default is 127.0.0.1:8080, or an available port if 8080 is not available. ($FUNC_ADDRESS)
--base-image string Override the base image for your function (host builder only)
--build string[="true"] Build the function. [auto|true|false]. ($FUNC_BUILD) (default "auto") --build string[="true"] Build the function. [auto|true|false]. ($FUNC_BUILD) (default "auto")
-b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". (default "pack") -b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". (default "pack")
--builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE) --builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)

View File

@ -152,6 +152,9 @@ type BuildSpec struct {
// in .func/built-image // in .func/built-image
Image string `yaml:"-"` Image string `yaml:"-"`
// BaseImage defines an override for the function to be built upon (host bulder only)
BaseImage string `yaml:"baseImage,omitempty"`
// Mounts used in build phase. This is useful in particular for paketo bindings. // Mounts used in build phase. This is useful in particular for paketo bindings.
Mounts []MountSpec `yaml:"volumes,omitempty"` Mounts []MountSpec `yaml:"volumes,omitempty"`
} }

View File

@ -64,7 +64,7 @@ type languageBuilder interface {
// Base returns the base image (if any) to use. Ideally this is a // Base returns the base image (if any) to use. Ideally this is a
// multi-arch base image with a corresponding platform image for // multi-arch base image with a corresponding platform image for
// each requested to be built. // each requested to be built.
Base() string Base(customBase string) string
// WriteShared layers (not platform-specific) which need to be genearted // WriteShared layers (not platform-specific) which need to be genearted
// on demand per language, such as shared dependencies. // on demand per language, such as shared dependencies.
@ -568,12 +568,13 @@ func newCertsTarball(source, target string, verbose bool) error {
// Its layers are automatically downloaded into the local cache if this is // Its layers are automatically downloaded into the local cache if this is
// the first fetch and their blobs linked into the final OCI image. // the first fetch and their blobs linked into the final OCI image.
func pullBase(job buildJob, p v1.Platform) (image v1.Image, err error) { func pullBase(job buildJob, p v1.Platform) (image v1.Image, err error) {
if job.languageBuilder.Base() == "" { baseImage := job.function.Build.BaseImage
if job.languageBuilder.Base(baseImage) == "" {
return // FROM SCRATCH return // FROM SCRATCH
} }
// Parse the base into a reference // Parse the base into a reference
ref, err := name.ParseReference(job.languageBuilder.Base()) ref, err := name.ParseReference(job.languageBuilder.Base(baseImage))
if err != nil { if err != nil {
return return
} }

View File

@ -461,7 +461,7 @@ func TestBuilder_StaticEnvs(t *testing.T) {
// OCI builder for each language, and can be overridden for testing // OCI builder for each language, and can be overridden for testing
type TestLanguageBuilder struct { type TestLanguageBuilder struct {
BaseInvoked bool BaseInvoked bool
BaseFn func() string BaseFn func(customImage string) string
WriteSharedInvoked bool WriteSharedInvoked bool
WriteSharedFn func(buildJob) ([]imageLayer, error) WriteSharedFn func(buildJob) ([]imageLayer, error)
@ -475,7 +475,7 @@ type TestLanguageBuilder struct {
func NewTestLanguageBuilder() *TestLanguageBuilder { func NewTestLanguageBuilder() *TestLanguageBuilder {
return &TestLanguageBuilder{ return &TestLanguageBuilder{
BaseFn: func() string { return "" }, BaseFn: func(customImage string) string { return "" },
WriteSharedFn: func(buildJob) ([]imageLayer, error) { return []imageLayer{}, nil }, WriteSharedFn: func(buildJob) ([]imageLayer, error) { return []imageLayer{}, nil },
WritePlatformFn: func(buildJob, v1.Platform) ([]imageLayer, error) { return []imageLayer{}, nil }, WritePlatformFn: func(buildJob, v1.Platform) ([]imageLayer, error) { return []imageLayer{}, nil },
ConfigureFn: func(buildJob, v1.Platform, v1.ConfigFile) (v1.ConfigFile, error) { ConfigureFn: func(buildJob, v1.Platform, v1.ConfigFile) (v1.ConfigFile, error) {
@ -484,9 +484,9 @@ func NewTestLanguageBuilder() *TestLanguageBuilder {
} }
} }
func (l *TestLanguageBuilder) Base() string { func (l *TestLanguageBuilder) Base(customImage string) string {
l.BaseInvoked = true l.BaseInvoked = true
return l.BaseFn() return l.BaseFn(customImage)
} }
func (l *TestLanguageBuilder) WriteShared(job buildJob) ([]imageLayer, error) { func (l *TestLanguageBuilder) WriteShared(job buildJob) ([]imageLayer, error) {

View File

@ -18,8 +18,9 @@ import (
type goBuilder struct{} type goBuilder struct{}
func (b goBuilder) Base() string { func (b goBuilder) Base(customImage string) string {
return "" // scratch // if not defined -> return "", meaning building from scratch
return customImage
} }
func (b goBuilder) Configure(_ buildJob, _ v1.Platform, cf v1.ConfigFile) (v1.ConfigFile, error) { func (b goBuilder) Configure(_ buildJob, _ v1.Platform, cf v1.ConfigFile) (v1.ConfigFile, error) {

View File

@ -19,7 +19,10 @@ var defaultPythonBase = "python:3.13-slim" // Moving from docker.io. See issue
type pythonBuilder struct{} type pythonBuilder struct{}
func (b pythonBuilder) Base() string { func (b pythonBuilder) Base(customBase string) string {
if customBase != "" {
return customBase
}
return defaultPythonBase return defaultPythonBase
} }

View File

@ -49,6 +49,10 @@
"type": "string", "type": "string",
"description": "RemoteStorageClass specifies the storage class to use for the volume used\non-cluster during when built remotely." "description": "RemoteStorageClass specifies the storage class to use for the volume used\non-cluster during when built remotely."
}, },
"baseImage": {
"type": "string",
"description": "BaseImage defines an override for the function to be built upon (host bulder only)"
},
"volumes": { "volumes": {
"items": { "items": {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",