Repository: kubeflow/pipelines

Issue:
[feature] Allow users to skip `docker build` in `kfp components build`
### Feature Area

<!-- Uncomment the labels below which are relevant to this feature: -->
<!-- /area frontend -->
<!-- /area backend -->
/area sdk
<!-- /area samples -->
<!-- /area components -->


### What feature would you like to see?

<!-- Provide a description of this feature and the user experience. -->

Building containerised components using the Python DSL, and packaging them using `kfp components build` is a really nice user experience - you get the benefits of the Python DSL without some of the drawbacks of "lightweight" components (e.g. having to install dependencies at runtime).

However, there are some scenarios where building container images using Docker is not an option, for example:

- Some organisations do not allow Docker to be installed on Data Scientists' laptops (very common for enterprise users)
- Some CI/CD platforms do not support docker-in-docker, and use other mechanisms (such as kaniko) for building container images

This feature would allow the user to skip the actual container image build, but still perform the other functions in `kfp components build` (generation of the different files). The user would then be responsible for building the container images using their preferred mechanism (e.g. Cloud Build, Kaniko)

### What is the use case or pain point?

<!-- It helps us understand the benefit of this feature for your use case. -->
Currently not all users are able to use the `kfp components build` command as they cannot run Docker (locally or in CI/CD)


### Is there a workaround currently?

<!-- Without this feature, how do you accomplish your task today? -->
Current workarounds:

- Roll your own containerised components without the convenience of the `kfp components build` command
- I suppose you could mock the `docker` library as is done in the tests? But this is quite hacky!

---

<!-- Don't delete message below to encourage users to support your feature request! -->
Love this idea? Give it a 👍. 


Discussion:
The feature request makes sense to me. In terms of implementation, have you considered adding support for different engines? https://github.com/kubeflow/pipelines/blob/1f5d8ff39759d1b305807939a90f3f58f4befb78/sdk/python/kfp/deprecated/cli/components.py#L72-L76
Hi Chen! Thanks for your  quick response!

I took a brief look at other container build engines. It looks like there is no official Python package for kaniko (although there is the unofficial [PyKaniko](https://github.com/nxexox/pykaniko)). Cloud Build could be supported using the Cloud Build Python SDK, but there are a lot of potential build options to account for! There are also a number of other build engines that users might prefer for one reason or another e.g. buildah.

I think an ideal solution would be to add support for more container build engines (tbd which ones we support in the SDK), and also to allow the user to skip the container build step and build the containers themselves (e.g. in a subsequent step of a CI/CD pipeline or bash script)

What do you think?
Makes sense to me. 
/cc @connor-mccarthy for a second opinion.
Agreed. Even with other build engine options, I think it makes sense to enable the "no engine" option also.

Resolving patch:
diff --git a/sdk/python/kfp/cli/component.py b/sdk/python/kfp/cli/component.py
--- a/sdk/python/kfp/cli/component.py
+++ b/sdk/python/kfp/cli/component.py
@@ -361,6 +361,12 @@ def component():
     default=False,
     help='Set this to true to always generate a Dockerfile'
     ' as part of the build process')
+@click.option(
+    '--build-image/--no-build-image',
+    type=bool,
+    is_flag=True,
+    default=True,
+    help='Build the container image.')
 @click.option(
     '--push-image/--no-push-image',
     type=bool,
@@ -369,10 +375,10 @@ def component():
     help='Push the built image to its remote repository.')
 def build(components_directory: str, component_filepattern: str, engine: str,
           kfp_package_path: Optional[str], overwrite_dockerfile: bool,
-          push_image: bool):
+          build_image: bool, push_image: bool):
     """Builds containers for KFP v2 Python-based components."""
 
-    if engine != 'docker':
+    if build_image and engine != 'docker':
         warnings.warn(
             'The --engine option is deprecated and does not need to be passed. Only Docker engine is supported and will be used by default.',
             DeprecationWarning,
@@ -387,12 +393,14 @@ def build(components_directory: str, component_filepattern: str, engine: str,
             f'{components_directory} does not seem to be a valid directory.')
         raise sys.exit(1)
 
-    if not _DOCKER_IS_PRESENT:
+    if build_image and not _DOCKER_IS_PRESENT:
         logging.error(
             'The `docker` Python package was not found in the current'
             ' environment. Please run `pip install docker` to install it.'
             ' Optionally, you can also  install KFP with all of its'
-            ' optional dependencies by running `pip install kfp[all]`.')
+            ' optional dependencies by running `pip install kfp[all]`.'
+            ' Alternatively, you can skip the container image build using'
+            ' the --no-build-image flag.')
         raise sys.exit(1)
 
     kfp_package_path = pathlib.Path(
@@ -408,4 +416,5 @@ def build(components_directory: str, component_filepattern: str, engine: str,
     builder.generate_requirements_txt()
     builder.maybe_generate_dockerignore()
     builder.maybe_generate_dockerfile(overwrite_dockerfile=overwrite_dockerfile)
-    builder.build_image(push_image=push_image)
+    if build_image:
+        builder.build_image(push_image=push_image)


Test patch:
diff --git a/sdk/python/kfp/cli/component_test.py b/sdk/python/kfp/cli/component_test.py
--- a/sdk/python/kfp/cli/component_test.py
+++ b/sdk/python/kfp/cli/component_test.py
@@ -386,6 +386,20 @@ def test_docker_client_is_called_to_build_and_push_by_default(self):
         self._docker_client.images.push.assert_called_once_with(
             'custom-image', stream=True, decode=True)
 
+    def test_docker_client_is_not_called_to_build_or_push(self):
+        component = _make_component(
+            func_name='train', target_image='custom-image')
+        _write_components('components.py', component)
+
+        result = self.runner.invoke(
+            self.cli,
+            ['build', str(self._working_dir), '--no-build-image'],
+        )
+        self.assertEqual(result.exit_code, 0)
+
+        self._docker_client.api.build.assert_not_called()
+        self._docker_client.images.push.assert_not_called()
+
     def test_docker_client_is_called_to_build_but_skips_pushing(self):
         component = _make_component(
             func_name='train', target_image='custom-image')