diff --git a/cmd/build.go b/cmd/build.go index 1ad9ef3631..d64a71a43c 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -69,8 +69,8 @@ EXAMPLES `, SuggestFor: []string{"biuld", "buidl", "built"}, PreRunE: bindEnv("image", "path", "builder", "registry", "confirm", - "push", "builder-image", "platform", "verbose", "build-timestamp", - "registry-insecure", "username", "password", "token"), + "push", "builder-image", "base-image", "platform", "verbose", + "build-timestamp", "registry-insecure", "username", "password", "token"), RunE: func(cmd *cobra.Command, args []string) error { return runBuild(cmd, args, newClient) }, @@ -110,6 +110,8 @@ EXAMPLES builderImage := f.Build.BuilderImages[f.Build.Builder] cmd.Flags().StringP("builder-image", "", builderImage, "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, "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 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 // working directory of the process. Path string @@ -260,6 +266,7 @@ func newBuildConfig() buildConfig { RegistryInsecure: viper.GetBool("registry-insecure"), }, BuilderImage: viper.GetString("builder-image"), + BaseImage: viper.GetString("base-image"), Image: viper.GetString("image"), Path: viper.GetString("path"), 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.Image = c.Image + f.Build.BaseImage = c.BaseImage // Path, Platform and Push are not part of a function's state. return f } @@ -360,6 +368,10 @@ func (c buildConfig) Validate() (err error) { 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 } diff --git a/cmd/build_test.go b/cmd/build_test.go index 02db1620a0..f0f5d11341 100644 --- a/cmd/build_test.go +++ b/cmd/build_test.go @@ -95,6 +95,12 @@ func TestBuild_Authentication(t *testing.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 // the --push flag. // - Push triggered after a successful build diff --git a/cmd/deploy.go b/cmd/deploy.go index 3f5d871818..fb35e44a19 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -128,7 +128,11 @@ EXAMPLES `, 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 { return runDeploy(cmd, newClient) }, @@ -163,6 +167,8 @@ EXAMPLES builderImage := f.Build.BuilderImages[f.Build.Builder] cmd.Flags().String("builder-image", builderImage, "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, "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)") diff --git a/cmd/deploy_test.go b/cmd/deploy_test.go index d75103a1d1..acd804a213 100644 --- a/cmd/deploy_test.go +++ b/cmd/deploy_test.go @@ -2170,3 +2170,75 @@ func Test_isDigested(t *testing.T) { 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")) + } + }) + } +} diff --git a/cmd/run.go b/cmd/run.go index a993b3f879..5533a76e39 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -77,7 +77,9 @@ EXAMPLES $ {{rootCmdUse}} run --json `, 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 { return runRun(cmd, newClient) }, @@ -109,6 +111,8 @@ EXAMPLES builderImage := f.Build.BuilderImages[f.Build.Builder] cmd.Flags().String("builder-image", builderImage, "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, "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{}, diff --git a/cmd/run_test.go b/cmd/run_test.go index 8eb0cd7528..08218cdab8 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -515,3 +515,89 @@ func TestRun_Address(t *testing.T) { 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) + } + }) + } +} diff --git a/docs/reference/func_build.md b/docs/reference/func_build.md index a58e41d706..fde126d2c6 100644 --- a/docs/reference/func_build.md +++ b/docs/reference/func_build.md @@ -57,6 +57,7 @@ func build ### 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. -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) diff --git a/docs/reference/func_deploy.md b/docs/reference/func_deploy.md index 71c7a49c22..1de14a1059 100644 --- a/docs/reference/func_deploy.md +++ b/docs/reference/func_deploy.md @@ -113,6 +113,7 @@ func deploy ### 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-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") diff --git a/docs/reference/func_run.md b/docs/reference/func_run.md index 1faabaf8a2..2f9a7cdb37 100644 --- a/docs/reference/func_run.md +++ b/docs/reference/func_run.md @@ -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) + --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") -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) diff --git a/pkg/functions/function.go b/pkg/functions/function.go index 0104f8ce42..e6b50128e6 100644 --- a/pkg/functions/function.go +++ b/pkg/functions/function.go @@ -152,6 +152,9 @@ type BuildSpec struct { // in .func/built-image 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 []MountSpec `yaml:"volumes,omitempty"` } diff --git a/pkg/oci/builder.go b/pkg/oci/builder.go index c2d0435a01..31379248df 100644 --- a/pkg/oci/builder.go +++ b/pkg/oci/builder.go @@ -64,7 +64,7 @@ type languageBuilder interface { // Base returns the base image (if any) to use. Ideally this is a // multi-arch base image with a corresponding platform image for // each requested to be built. - Base() string + Base(customBase string) string // WriteShared layers (not platform-specific) which need to be genearted // 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 // the first fetch and their blobs linked into the final OCI image. 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 } // Parse the base into a reference - ref, err := name.ParseReference(job.languageBuilder.Base()) + ref, err := name.ParseReference(job.languageBuilder.Base(baseImage)) if err != nil { return } diff --git a/pkg/oci/builder_test.go b/pkg/oci/builder_test.go index ddc41ac909..9b0561b2c5 100644 --- a/pkg/oci/builder_test.go +++ b/pkg/oci/builder_test.go @@ -461,7 +461,7 @@ func TestBuilder_StaticEnvs(t *testing.T) { // OCI builder for each language, and can be overridden for testing type TestLanguageBuilder struct { BaseInvoked bool - BaseFn func() string + BaseFn func(customImage string) string WriteSharedInvoked bool WriteSharedFn func(buildJob) ([]imageLayer, error) @@ -475,7 +475,7 @@ type TestLanguageBuilder struct { func NewTestLanguageBuilder() *TestLanguageBuilder { return &TestLanguageBuilder{ - BaseFn: func() string { return "" }, + BaseFn: func(customImage string) string { return "" }, WriteSharedFn: func(buildJob) ([]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) { @@ -484,9 +484,9 @@ func NewTestLanguageBuilder() *TestLanguageBuilder { } } -func (l *TestLanguageBuilder) Base() string { +func (l *TestLanguageBuilder) Base(customImage string) string { l.BaseInvoked = true - return l.BaseFn() + return l.BaseFn(customImage) } func (l *TestLanguageBuilder) WriteShared(job buildJob) ([]imageLayer, error) { diff --git a/pkg/oci/go_builder.go b/pkg/oci/go_builder.go index f6fdad4093..13a85e826b 100644 --- a/pkg/oci/go_builder.go +++ b/pkg/oci/go_builder.go @@ -18,8 +18,9 @@ import ( type goBuilder struct{} -func (b goBuilder) Base() string { - return "" // scratch +func (b goBuilder) Base(customImage string) string { + // if not defined -> return "", meaning building from scratch + return customImage } func (b goBuilder) Configure(_ buildJob, _ v1.Platform, cf v1.ConfigFile) (v1.ConfigFile, error) { diff --git a/pkg/oci/python_builder.go b/pkg/oci/python_builder.go index a7d093f4ef..3cc3126559 100644 --- a/pkg/oci/python_builder.go +++ b/pkg/oci/python_builder.go @@ -19,7 +19,10 @@ var defaultPythonBase = "python:3.13-slim" // Moving from docker.io. See issue type pythonBuilder struct{} -func (b pythonBuilder) Base() string { +func (b pythonBuilder) Base(customBase string) string { + if customBase != "" { + return customBase + } return defaultPythonBase } diff --git a/schema/func_yaml-schema.json b/schema/func_yaml-schema.json index ff0fe8d00f..0d8cd32dea 100644 --- a/schema/func_yaml-schema.json +++ b/schema/func_yaml-schema.json @@ -49,6 +49,10 @@ "type": "string", "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": { "items": { "$schema": "http://json-schema.org/draft-04/schema#",