Repository: kubeflow/pipelines

Issue:
Unresolved PipelineParam when static list passed to dsl.ParallelFor contains pipelineParam
### What steps did you take:
When a dsl.ParallelFor is created with static list containing a pipeline param, the compiler fails with error message: `RuntimeError: Internal compiler error: Found unresolved PipelineParam.`

### What happened:

You can reproduce with the following code:
```python
from kfp import dsl, components, compiler
def print_op(s: str):
    print(f"a is {s}")
    
PRINT_OP = components.func_to_container_op(print_op, base_image="python:3.7")

@dsl.pipeline(name="test pipeline")
def my_pipeline(name: str):
    
    texts = [{"val":"toot"}, {"val": str(name)}]
    
    with dsl.ParallelFor(texts) as text:
        PRINT_OP(s=text.val)

compiler_instance = compiler.Compiler()
compiler_instance.compile(pipeline_func=my_pipeline, package_path="my_pipeline.yaml")
```
Here is the error:
![image](https://user-images.githubusercontent.com/5963615/101994778-52c81400-3cc5-11eb-82de-018811d4eab2.png)

### What did you expect to happen:
I expect it to compile.

### Environment:
<!-- Please fill in those that seem relevant. -->

How did you deploy Kubeflow Pipelines (KFP)?
Kubeflow deployed on EKS

KFP version: `1.0.4`

KFP SDK version: `1.0.4`

### Anything else you would like to add:
I analysed YAML and it seems the pipeline param is not resolved inside YAML.

/kind bug
/area sdk


Discussion:

Resolving patch:
diff --git a/sdk/python/kfp/compiler/compiler.py b/sdk/python/kfp/compiler/compiler.py
--- a/sdk/python/kfp/compiler/compiler.py
+++ b/sdk/python/kfp/compiler/compiler.py
@@ -28,13 +28,14 @@
 
 from .. import dsl
 from ._k8s_helper import convert_k8s_obj_to_json, sanitize_k8s_name
-from ._op_to_template import _op_to_template
+from ._op_to_template import _op_to_template, _process_obj
 from ._default_transformers import add_pod_env
 
 from ..components.structures import InputSpec
 from ..components._yaml_utils import dump_yaml
 from ..dsl._metadata import _extract_pipeline_metadata
 from ..dsl._ops_group import OpsGroup
+from ..dsl._pipeline_param import extract_pipelineparams_from_any, PipelineParam
 
 
 class Compiler(object):
@@ -422,6 +423,15 @@ def _resolve_value_or_reference(self, value_or_reference, potential_references):
     else:
       return str(value_or_reference)
 
+  @staticmethod
+  def _resolve_task_pipeline_param(pipeline_param: PipelineParam, group_type) -> str:
+    if pipeline_param.op_name is None:
+      return '{{workflow.parameters.%s}}' % pipeline_param.name
+    param_name = '%s-%s' % (sanitize_k8s_name(pipeline_param.op_name), pipeline_param.name)
+    if group_type == 'subgraph':
+      return '{{inputs.parameters.%s}}' % (param_name)
+    return '{{tasks.%s.outputs.parameters.%s}}' % (sanitize_k8s_name(pipeline_param.op_name), param_name)
+
   def _group_to_dag_template(self, group, inputs, outputs, dependencies):
     """Generate template given an OpsGroup.
 
@@ -496,19 +506,8 @@ def _group_to_dag_template(self, group, inputs, outputs, dependencies):
           # as global pipeline parameters
 
           pipeline_param = sub_group.loop_args.items_or_pipeline_param
-          if pipeline_param.op_name is None:
-            withparam_value = '{{workflow.parameters.%s}}' % pipeline_param.name
-          else:
-            param_name = '%s-%s' % (
-              sanitize_k8s_name(pipeline_param.op_name), pipeline_param.name)
-            # [TODO] create ENUM or string type to represent all well-known group types in the DSL. 
-            if group.type == 'subgraph':
-              withparam_value = '{{inputs.parameters.%s}}' % (param_name)
-            else:
-              withparam_value = '{{tasks.%s.outputs.parameters.%s}}' % (
-                sanitize_k8s_name(pipeline_param.op_name),
-                param_name)
-
+          withparam_value = self._resolve_task_pipeline_param(pipeline_param, group.type)
+          if pipeline_param.op_name:
             # these loop args are the output of another task
             if 'dependencies' not in task or task['dependencies'] is None:
               task['dependencies'] = []
@@ -521,6 +520,20 @@ def _group_to_dag_template(self, group, inputs, outputs, dependencies):
         else:
           # Need to sanitize the dict keys for consistency.
           loop_tasks = sub_group.loop_args.to_list_for_task_yaml()
+          nested_pipeline_params = extract_pipelineparams_from_any(loop_tasks)
+
+          # Set dependencies in case of nested pipeline_params
+          map_to_tmpl_var = {str(p): self._resolve_task_pipeline_param(p, group.type) for p in nested_pipeline_params}
+          for pipeline_param in nested_pipeline_params:
+            if pipeline_param.op_name:
+              # these pipeline_param are the output of another task
+              if 'dependencies' not in task or task['dependencies'] is None:
+                task['dependencies'] = []
+              if sanitize_k8s_name(
+                  pipeline_param.op_name) not in task['dependencies']:
+                task['dependencies'].append(
+                    sanitize_k8s_name(pipeline_param.op_name))
+
           sanitized_tasks = []
           if isinstance(loop_tasks[0], dict):
             for argument_set in loop_tasks:
@@ -530,7 +543,12 @@ def _group_to_dag_template(self, group, inputs, outputs, dependencies):
               sanitized_tasks.append(c_dict)
           else:
             sanitized_tasks = loop_tasks
-          task['withItems'] = sanitized_tasks
+          # Replace pipeline param if map_to_tmpl_var not empty
+          task['withItems'] = _process_obj(sanitized_tasks, map_to_tmpl_var) if map_to_tmpl_var else sanitized_tasks
+
+        # We will sort dependencies to have determinitc yaml and thus stable tests
+        if task.get('dependencies'):
+            task['dependencies'].sort()
 
       tasks.append(task)
     tasks.sort(key=lambda x: x['name'])


Test patch:
diff --git a/sdk/python/tests/compiler/compiler_tests.py b/sdk/python/tests/compiler/compiler_tests.py
--- a/sdk/python/tests/compiler/compiler_tests.py
+++ b/sdk/python/tests/compiler/compiler_tests.py
@@ -26,8 +26,9 @@
 import unittest
 import yaml
 
+from kfp.compiler import Compiler
 from kfp.dsl._component import component
-from kfp.dsl import ContainerOp, pipeline
+from kfp.dsl import ContainerOp, pipeline, PipelineParam
 from kfp.dsl.types import Integer, InconsistentTypeException
 from kubernetes.client import V1Toleration, V1Affinity, V1NodeSelector, V1NodeSelectorRequirement, V1NodeSelectorTerm, \
   V1NodeAffinity, V1PodDNSConfig, V1PodDNSConfigOption
@@ -938,6 +939,9 @@ def test_withparam_output_dict(self):
   def test_withparam_lightweight_out(self):
     self._test_py_compile_yaml('loop_over_lightweight_output')
 
+  def test_parallelfor_pipeline_param_in_items_resolving(self):
+    self._test_py_compile_yaml('parallelfor_pipeline_param_in_items_resolving')
+
   def test_parallelfor_item_argument_resolving(self):
     self._test_py_compile_yaml('parallelfor_item_argument_resolving')
 
@@ -1089,5 +1093,18 @@ def some_pipeline():
       parameter_arguments = json.loads(parameter_arguments_json)
       self.assertEqual(set(parameter_arguments.keys()), {'Input 1'})
 
+  def test__resolve_task_pipeline_param(self):
+    p = PipelineParam(name='param2')
+    resolved = Compiler._resolve_task_pipeline_param(p, group_type=None)
+    self.assertEqual(resolved, "{{workflow.parameters.param2}}")
+
+    p = PipelineParam(name='param1', op_name='op1')
+    resolved = Compiler._resolve_task_pipeline_param(p, group_type=None)
+    self.assertEqual(resolved, "{{tasks.op1.outputs.parameters.op1-param1}}")
+
+    p = PipelineParam(name='param1', op_name='op1')
+    resolved = Compiler._resolve_task_pipeline_param(p, group_type="subgraph")
+    self.assertEqual(resolved, "{{inputs.parameters.op1-param1}}")
+
   def test_uri_artifact_passing(self):
     self._test_py_compile_yaml('uri_artifacts')
diff --git a/sdk/python/tests/compiler/testdata/parallelfor_pipeline_param_in_items_resolving.py b/sdk/python/tests/compiler/testdata/parallelfor_pipeline_param_in_items_resolving.py
new file mode 100644
--- /dev/null
+++ b/sdk/python/tests/compiler/testdata/parallelfor_pipeline_param_in_items_resolving.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import NamedTuple
+
+import kfp
+import kfp.compiler as compiler
+from kfp.components import func_to_container_op
+
+
+# Stabilizing the test output
+class StableIDGenerator:
+    def __init__(self, ):
+        self._index = 0
+
+    def get_next_id(self, ):
+        self._index += 1
+        return '{code:0{num_chars:}d}'.format(code=self._index, num_chars=kfp.dsl._for_loop.LoopArguments.NUM_CODE_CHARS)
+
+kfp.dsl.ParallelFor._get_unique_id_code = StableIDGenerator().get_next_id
+
+
+@func_to_container_op
+def produce_message(fname1: str) -> str:
+    return "My name is %s" % fname1
+
+@func_to_container_op
+def consume(param1):
+    print(param1)
+
+
+@kfp.dsl.pipeline()
+def parallelfor_pipeline_param_in_items_resolving(fname1: str, fname2: str):
+
+    simple_list = ["My name is %s" % fname1,
+                   "My name is %s" % fname2]
+
+    list_of_dict = [{"first_name": fname1, "message": "My name is %s" % fname1},
+                    {"first_name": fname2, "message": "My name is %s" % fname2}]
+
+    list_of_complex_dict = [
+        {"first_name": fname1, "message": produce_message(fname1).output},
+        {"first_name": fname2, "message": produce_message(fname2).output}]
+    
+    with kfp.dsl.ParallelFor(simple_list) as loop_item:
+        consume(loop_item)
+
+    with kfp.dsl.ParallelFor(list_of_dict) as loop_item2:
+        consume(loop_item2.first_name)
+        consume(loop_item2.message)
+
+    with kfp.dsl.ParallelFor(list_of_complex_dict) as loop_item:
+        consume(loop_item.first_name)
+        consume(loop_item.message)
+
+
+if __name__ == '__main__':
+    import kfp.compiler as compiler
+    compiler.Compiler().compile(parallelfor_pipeline_param_in_items_resolving, __file__ + '.yaml')
diff --git a/sdk/python/tests/compiler/testdata/parallelfor_pipeline_param_in_items_resolving.yaml b/sdk/python/tests/compiler/testdata/parallelfor_pipeline_param_in_items_resolving.yaml
new file mode 100644
--- /dev/null
+++ b/sdk/python/tests/compiler/testdata/parallelfor_pipeline_param_in_items_resolving.yaml
@@ -0,0 +1,425 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Workflow
+metadata:
+  generateName: parallelfor-pipeline-param-in-items-resolving-
+  annotations: {pipelines.kubeflow.org/kfp_sdk_version: 1.2.0, pipelines.kubeflow.org/pipeline_compilation_time: '2020-12-22T10:30:28.553559',
+    pipelines.kubeflow.org/pipeline_spec: '{"inputs": [{"name": "fname1", "type":
+      "String"}, {"name": "fname2", "type": "String"}], "name": "Parallelfor pipeline
+      param in items resolving"}'}
+  labels: {pipelines.kubeflow.org/kfp_sdk_version: 1.2.0}
+spec:
+  entrypoint: parallelfor-pipeline-param-in-items-resolving
+  templates:
+  - name: consume
+    container:
+      args: [--param1, '{{inputs.parameters.loop-item-param-00000001}}']
+      command:
+      - sh
+      - -ec
+      - |
+        program_path=$(mktemp)
+        echo -n "$0" > "$program_path"
+        python3 -u "$program_path" "$@"
+      - |
+        def consume(param1):
+            print(param1)
+
+        import argparse
+        _parser = argparse.ArgumentParser(prog='Consume', description='')
+        _parser.add_argument("--param1", dest="param1", type=str, required=True, default=argparse.SUPPRESS)
+        _parsed_args = vars(_parser.parse_args())
+
+        _outputs = consume(**_parsed_args)
+      image: python:3.7
+    inputs:
+      parameters:
+      - {name: loop-item-param-00000001}
+    metadata:
+      annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
+          {"args": ["--param1", {"inputValue": "param1"}], "command": ["sh", "-ec",
+          "program_path=$(mktemp)\necho -n \"$0\" > \"$program_path\"\npython3 -u
+          \"$program_path\" \"$@\"\n", "def consume(param1):\n    print(param1)\n\nimport
+          argparse\n_parser = argparse.ArgumentParser(prog=''Consume'', description='''')\n_parser.add_argument(\"--param1\",
+          dest=\"param1\", type=str, required=True, default=argparse.SUPPRESS)\n_parsed_args
+          = vars(_parser.parse_args())\n\n_outputs = consume(**_parsed_args)\n"],
+          "image": "python:3.7"}}, "inputs": [{"name": "param1"}], "name": "Consume"}',
+        pipelines.kubeflow.org/component_ref: '{}', pipelines.kubeflow.org/arguments.parameters: '{"param1":
+          "{{inputs.parameters.loop-item-param-00000001}}"}'}
+  - name: consume-2
+    container:
+      args: [--param1, '{{inputs.parameters.loop-item-param-00000002-subvar-first_name}}']
+      command:
+      - sh
+      - -ec
+      - |
+        program_path=$(mktemp)
+        echo -n "$0" > "$program_path"
+        python3 -u "$program_path" "$@"
+      - |
+        def consume(param1):
+            print(param1)
+
+        import argparse
+        _parser = argparse.ArgumentParser(prog='Consume', description='')
+        _parser.add_argument("--param1", dest="param1", type=str, required=True, default=argparse.SUPPRESS)
+        _parsed_args = vars(_parser.parse_args())
+
+        _outputs = consume(**_parsed_args)
+      image: python:3.7
+    inputs:
+      parameters:
+      - {name: loop-item-param-00000002-subvar-first_name}
+    metadata:
+      annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
+          {"args": ["--param1", {"inputValue": "param1"}], "command": ["sh", "-ec",
+          "program_path=$(mktemp)\necho -n \"$0\" > \"$program_path\"\npython3 -u
+          \"$program_path\" \"$@\"\n", "def consume(param1):\n    print(param1)\n\nimport
+          argparse\n_parser = argparse.ArgumentParser(prog=''Consume'', description='''')\n_parser.add_argument(\"--param1\",
+          dest=\"param1\", type=str, required=True, default=argparse.SUPPRESS)\n_parsed_args
+          = vars(_parser.parse_args())\n\n_outputs = consume(**_parsed_args)\n"],
+          "image": "python:3.7"}}, "inputs": [{"name": "param1"}], "name": "Consume"}',
+        pipelines.kubeflow.org/component_ref: '{}', pipelines.kubeflow.org/arguments.parameters: '{"param1":
+          "{{inputs.parameters.loop-item-param-00000002-subvar-first_name}}"}'}
+  - name: consume-3
+    container:
+      args: [--param1, '{{inputs.parameters.loop-item-param-00000002-subvar-message}}']
+      command:
+      - sh
+      - -ec
+      - |
+        program_path=$(mktemp)
+        echo -n "$0" > "$program_path"
+        python3 -u "$program_path" "$@"
+      - |
+        def consume(param1):
+            print(param1)
+
+        import argparse
+        _parser = argparse.ArgumentParser(prog='Consume', description='')
+        _parser.add_argument("--param1", dest="param1", type=str, required=True, default=argparse.SUPPRESS)
+        _parsed_args = vars(_parser.parse_args())
+
+        _outputs = consume(**_parsed_args)
+      image: python:3.7
+    inputs:
+      parameters:
+      - {name: loop-item-param-00000002-subvar-message}
+    metadata:
+      annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
+          {"args": ["--param1", {"inputValue": "param1"}], "command": ["sh", "-ec",
+          "program_path=$(mktemp)\necho -n \"$0\" > \"$program_path\"\npython3 -u
+          \"$program_path\" \"$@\"\n", "def consume(param1):\n    print(param1)\n\nimport
+          argparse\n_parser = argparse.ArgumentParser(prog=''Consume'', description='''')\n_parser.add_argument(\"--param1\",
+          dest=\"param1\", type=str, required=True, default=argparse.SUPPRESS)\n_parsed_args
+          = vars(_parser.parse_args())\n\n_outputs = consume(**_parsed_args)\n"],
+          "image": "python:3.7"}}, "inputs": [{"name": "param1"}], "name": "Consume"}',
+        pipelines.kubeflow.org/component_ref: '{}', pipelines.kubeflow.org/arguments.parameters: '{"param1":
+          "{{inputs.parameters.loop-item-param-00000002-subvar-message}}"}'}
+  - name: consume-4
+    container:
+      args: [--param1, '{{inputs.parameters.loop-item-param-00000003-subvar-first_name}}']
+      command:
+      - sh
+      - -ec
+      - |
+        program_path=$(mktemp)
+        echo -n "$0" > "$program_path"
+        python3 -u "$program_path" "$@"
+      - |
+        def consume(param1):
+            print(param1)
+
+        import argparse
+        _parser = argparse.ArgumentParser(prog='Consume', description='')
+        _parser.add_argument("--param1", dest="param1", type=str, required=True, default=argparse.SUPPRESS)
+        _parsed_args = vars(_parser.parse_args())
+
+        _outputs = consume(**_parsed_args)
+      image: python:3.7
+    inputs:
+      parameters:
+      - {name: loop-item-param-00000003-subvar-first_name}
+    metadata:
+      annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
+          {"args": ["--param1", {"inputValue": "param1"}], "command": ["sh", "-ec",
+          "program_path=$(mktemp)\necho -n \"$0\" > \"$program_path\"\npython3 -u
+          \"$program_path\" \"$@\"\n", "def consume(param1):\n    print(param1)\n\nimport
+          argparse\n_parser = argparse.ArgumentParser(prog=''Consume'', description='''')\n_parser.add_argument(\"--param1\",
+          dest=\"param1\", type=str, required=True, default=argparse.SUPPRESS)\n_parsed_args
+          = vars(_parser.parse_args())\n\n_outputs = consume(**_parsed_args)\n"],
+          "image": "python:3.7"}}, "inputs": [{"name": "param1"}], "name": "Consume"}',
+        pipelines.kubeflow.org/component_ref: '{}', pipelines.kubeflow.org/arguments.parameters: '{"param1":
+          "{{inputs.parameters.loop-item-param-00000003-subvar-first_name}}"}'}
+  - name: consume-5
+    container:
+      args: [--param1, '{{inputs.parameters.loop-item-param-00000003-subvar-message}}']
+      command:
+      - sh
+      - -ec
+      - |
+        program_path=$(mktemp)
+        echo -n "$0" > "$program_path"
+        python3 -u "$program_path" "$@"
+      - |
+        def consume(param1):
+            print(param1)
+
+        import argparse
+        _parser = argparse.ArgumentParser(prog='Consume', description='')
+        _parser.add_argument("--param1", dest="param1", type=str, required=True, default=argparse.SUPPRESS)
+        _parsed_args = vars(_parser.parse_args())
+
+        _outputs = consume(**_parsed_args)
+      image: python:3.7
+    inputs:
+      parameters:
+      - {name: loop-item-param-00000003-subvar-message}
+    metadata:
+      annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
+          {"args": ["--param1", {"inputValue": "param1"}], "command": ["sh", "-ec",
+          "program_path=$(mktemp)\necho -n \"$0\" > \"$program_path\"\npython3 -u
+          \"$program_path\" \"$@\"\n", "def consume(param1):\n    print(param1)\n\nimport
+          argparse\n_parser = argparse.ArgumentParser(prog=''Consume'', description='''')\n_parser.add_argument(\"--param1\",
+          dest=\"param1\", type=str, required=True, default=argparse.SUPPRESS)\n_parsed_args
+          = vars(_parser.parse_args())\n\n_outputs = consume(**_parsed_args)\n"],
+          "image": "python:3.7"}}, "inputs": [{"name": "param1"}], "name": "Consume"}',
+        pipelines.kubeflow.org/component_ref: '{}', pipelines.kubeflow.org/arguments.parameters: '{"param1":
+          "{{inputs.parameters.loop-item-param-00000003-subvar-message}}"}'}
+  - name: for-loop-for-loop-00000001-1
+    inputs:
+      parameters:
+      - {name: loop-item-param-00000001}
+    dag:
+      tasks:
+      - name: consume
+        template: consume
+        arguments:
+          parameters:
+          - {name: loop-item-param-00000001, value: '{{inputs.parameters.loop-item-param-00000001}}'}
+  - name: for-loop-for-loop-00000002-2
+    inputs:
+      parameters:
+      - {name: loop-item-param-00000002-subvar-first_name}
+      - {name: loop-item-param-00000002-subvar-message}
+    dag:
+      tasks:
+      - name: consume-2
+        template: consume-2
+        arguments:
+          parameters:
+          - {name: loop-item-param-00000002-subvar-first_name, value: '{{inputs.parameters.loop-item-param-00000002-subvar-first_name}}'}
+      - name: consume-3
+        template: consume-3
+        arguments:
+          parameters:
+          - {name: loop-item-param-00000002-subvar-message, value: '{{inputs.parameters.loop-item-param-00000002-subvar-message}}'}
+  - name: for-loop-for-loop-00000003-3
+    inputs:
+      parameters:
+      - {name: loop-item-param-00000003-subvar-first_name}
+      - {name: loop-item-param-00000003-subvar-message}
+    dag:
+      tasks:
+      - name: consume-4
+        template: consume-4
+        arguments:
+          parameters:
+          - {name: loop-item-param-00000003-subvar-first_name, value: '{{inputs.parameters.loop-item-param-00000003-subvar-first_name}}'}
+      - name: consume-5
+        template: consume-5
+        arguments:
+          parameters:
+          - {name: loop-item-param-00000003-subvar-message, value: '{{inputs.parameters.loop-item-param-00000003-subvar-message}}'}
+  - name: parallelfor-pipeline-param-in-items-resolving
+    inputs:
+      parameters:
+      - {name: fname1}
+      - {name: fname2}
+    dag:
+      tasks:
+      - name: for-loop-for-loop-00000001-1
+        template: for-loop-for-loop-00000001-1
+        arguments:
+          parameters:
+          - {name: loop-item-param-00000001, value: '{{item}}'}
+        withItems: ['My name is {{workflow.parameters.fname1}}', 'My name is {{workflow.parameters.fname2}}']
+      - name: for-loop-for-loop-00000002-2
+        template: for-loop-for-loop-00000002-2
+        arguments:
+          parameters:
+          - {name: loop-item-param-00000002-subvar-first_name, value: '{{item.first_name}}'}
+          - {name: loop-item-param-00000002-subvar-message, value: '{{item.message}}'}
+        withItems:
+        - {first_name: '{{workflow.parameters.fname1}}', message: 'My name is {{workflow.parameters.fname1}}'}
+        - {first_name: '{{workflow.parameters.fname2}}', message: 'My name is {{workflow.parameters.fname2}}'}
+      - name: for-loop-for-loop-00000003-3
+        template: for-loop-for-loop-00000003-3
+        arguments:
+          parameters:
+          - {name: loop-item-param-00000003-subvar-first_name, value: '{{item.first_name}}'}
+          - {name: loop-item-param-00000003-subvar-message, value: '{{item.message}}'}
+        dependencies: [produce-message, produce-message-2]
+        withItems:
+        - {first_name: '{{workflow.parameters.fname1}}', message: '{{tasks.produce-message.outputs.parameters.produce-message-Output}}'}
+        - {first_name: '{{workflow.parameters.fname2}}', message: '{{tasks.produce-message-2.outputs.parameters.produce-message-2-Output}}'}
+      - name: produce-message
+        template: produce-message
+        arguments:
+          parameters:
+          - {name: fname1, value: '{{inputs.parameters.fname1}}'}
+      - name: produce-message-2
+        template: produce-message-2
+        arguments:
+          parameters:
+          - {name: fname2, value: '{{inputs.parameters.fname2}}'}
+  - name: produce-message
+    container:
+      args: [--fname1, '{{inputs.parameters.fname1}}', '----output-paths', /tmp/outputs/Output/data]
+      command:
+      - sh
+      - -ec
+      - |
+        program_path=$(mktemp)
+        echo -n "$0" > "$program_path"
+        python3 -u "$program_path" "$@"
+      - |
+        def produce_message(fname1):
+            return "My name is %s" % fname1
+
+        def _serialize_str(str_value: str) -> str:
+            if not isinstance(str_value, str):
+                raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value))))
+            return str_value
+
+        import argparse
+        _parser = argparse.ArgumentParser(prog='Produce message', description='')
+        _parser.add_argument("--fname1", dest="fname1", type=str, required=True, default=argparse.SUPPRESS)
+        _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=1)
+        _parsed_args = vars(_parser.parse_args())
+        _output_files = _parsed_args.pop("_output_paths", [])
+
+        _outputs = produce_message(**_parsed_args)
+
+        _outputs = [_outputs]
+
+        _output_serializers = [
+            _serialize_str,
+
+        ]
+
+        import os
+        for idx, output_file in enumerate(_output_files):
+            try:
+                os.makedirs(os.path.dirname(output_file))
+            except OSError:
+                pass
+            with open(output_file, 'w') as f:
+                f.write(_output_serializers[idx](_outputs[idx]))
+      image: python:3.7
+    inputs:
+      parameters:
+      - {name: fname1}
+    outputs:
+      parameters:
+      - name: produce-message-Output
+        valueFrom: {path: /tmp/outputs/Output/data}
+      artifacts:
+      - {name: produce-message-Output, path: /tmp/outputs/Output/data}
+    metadata:
+      annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
+          {"args": ["--fname1", {"inputValue": "fname1"}, "----output-paths", {"outputPath":
+          "Output"}], "command": ["sh", "-ec", "program_path=$(mktemp)\necho -n \"$0\"
+          > \"$program_path\"\npython3 -u \"$program_path\" \"$@\"\n", "def produce_message(fname1):\n    return
+          \"My name is %s\" % fname1\n\ndef _serialize_str(str_value: str) -> str:\n    if
+          not isinstance(str_value, str):\n        raise TypeError(''Value \"{}\"
+          has type \"{}\" instead of str.''.format(str(str_value), str(type(str_value))))\n    return
+          str_value\n\nimport argparse\n_parser = argparse.ArgumentParser(prog=''Produce
+          message'', description='''')\n_parser.add_argument(\"--fname1\", dest=\"fname1\",
+          type=str, required=True, default=argparse.SUPPRESS)\n_parser.add_argument(\"----output-paths\",
+          dest=\"_output_paths\", type=str, nargs=1)\n_parsed_args = vars(_parser.parse_args())\n_output_files
+          = _parsed_args.pop(\"_output_paths\", [])\n\n_outputs = produce_message(**_parsed_args)\n\n_outputs
+          = [_outputs]\n\n_output_serializers = [\n    _serialize_str,\n\n]\n\nimport
+          os\nfor idx, output_file in enumerate(_output_files):\n    try:\n        os.makedirs(os.path.dirname(output_file))\n    except
+          OSError:\n        pass\n    with open(output_file, ''w'') as f:\n        f.write(_output_serializers[idx](_outputs[idx]))\n"],
+          "image": "python:3.7"}}, "inputs": [{"name": "fname1", "type": "String"}],
+          "name": "Produce message", "outputs": [{"name": "Output", "type": "String"}]}',
+        pipelines.kubeflow.org/component_ref: '{}', pipelines.kubeflow.org/arguments.parameters: '{"fname1":
+          "{{inputs.parameters.fname1}}"}'}
+  - name: produce-message-2
+    container:
+      args: [--fname1, '{{inputs.parameters.fname2}}', '----output-paths', /tmp/outputs/Output/data]
+      command:
+      - sh
+      - -ec
+      - |
+        program_path=$(mktemp)
+        echo -n "$0" > "$program_path"
+        python3 -u "$program_path" "$@"
+      - |
+        def produce_message(fname1):
+            return "My name is %s" % fname1
+
+        def _serialize_str(str_value: str) -> str:
+            if not isinstance(str_value, str):
+                raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value))))
+            return str_value
+
+        import argparse
+        _parser = argparse.ArgumentParser(prog='Produce message', description='')
+        _parser.add_argument("--fname1", dest="fname1", type=str, required=True, default=argparse.SUPPRESS)
+        _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=1)
+        _parsed_args = vars(_parser.parse_args())
+        _output_files = _parsed_args.pop("_output_paths", [])
+
+        _outputs = produce_message(**_parsed_args)
+
+        _outputs = [_outputs]
+
+        _output_serializers = [
+            _serialize_str,
+
+        ]
+
+        import os
+        for idx, output_file in enumerate(_output_files):
+            try:
+                os.makedirs(os.path.dirname(output_file))
+            except OSError:
+                pass
+            with open(output_file, 'w') as f:
+                f.write(_output_serializers[idx](_outputs[idx]))
+      image: python:3.7
+    inputs:
+      parameters:
+      - {name: fname2}
+    outputs:
+      parameters:
+      - name: produce-message-2-Output
+        valueFrom: {path: /tmp/outputs/Output/data}
+      artifacts:
+      - {name: produce-message-2-Output, path: /tmp/outputs/Output/data}
+    metadata:
+      annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
+          {"args": ["--fname1", {"inputValue": "fname1"}, "----output-paths", {"outputPath":
+          "Output"}], "command": ["sh", "-ec", "program_path=$(mktemp)\necho -n \"$0\"
+          > \"$program_path\"\npython3 -u \"$program_path\" \"$@\"\n", "def produce_message(fname1):\n    return
+          \"My name is %s\" % fname1\n\ndef _serialize_str(str_value: str) -> str:\n    if
+          not isinstance(str_value, str):\n        raise TypeError(''Value \"{}\"
+          has type \"{}\" instead of str.''.format(str(str_value), str(type(str_value))))\n    return
+          str_value\n\nimport argparse\n_parser = argparse.ArgumentParser(prog=''Produce
+          message'', description='''')\n_parser.add_argument(\"--fname1\", dest=\"fname1\",
+          type=str, required=True, default=argparse.SUPPRESS)\n_parser.add_argument(\"----output-paths\",
+          dest=\"_output_paths\", type=str, nargs=1)\n_parsed_args = vars(_parser.parse_args())\n_output_files
+          = _parsed_args.pop(\"_output_paths\", [])\n\n_outputs = produce_message(**_parsed_args)\n\n_outputs
+          = [_outputs]\n\n_output_serializers = [\n    _serialize_str,\n\n]\n\nimport
+          os\nfor idx, output_file in enumerate(_output_files):\n    try:\n        os.makedirs(os.path.dirname(output_file))\n    except
+          OSError:\n        pass\n    with open(output_file, ''w'') as f:\n        f.write(_output_serializers[idx](_outputs[idx]))\n"],
+          "image": "python:3.7"}}, "inputs": [{"name": "fname1", "type": "String"}],
+          "name": "Produce message", "outputs": [{"name": "Output", "type": "String"}]}',
+        pipelines.kubeflow.org/component_ref: '{}', pipelines.kubeflow.org/arguments.parameters: '{"fname1":
+          "{{inputs.parameters.fname2}}"}'}
+  arguments:
+    parameters:
+    - {name: fname1}
+    - {name: fname2}
+  serviceAccountName: pipeline-runner