Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Backend + SDK): Update kfp backend and kubernetes sdk to support ConfigMaps as volumes and as env variables #10483

Merged
merged 11 commits into from Feb 24, 2024
33 changes: 33 additions & 0 deletions backend/src/v2/driver/driver.go
Expand Up @@ -512,6 +512,39 @@ func extendPodSpecPatch(
}
}

// Get config map mount information
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe we can refactor these two for loops into functions so that we can also reuse them for secret mount and secret env

for _, configMapAsVolume := range kubernetesExecutorConfig.GetConfigMapAsVolume() {
configMapVolume := k8score.Volume{
Name: configMapAsVolume.GetConfigMapName(),
VolumeSource: k8score.VolumeSource{
ConfigMap: &k8score.ConfigMapVolumeSource{
LocalObjectReference: k8score.LocalObjectReference{Name: configMapAsVolume.GetConfigMapName()}},
},
}
configMapVolumeMount := k8score.VolumeMount{
Name: configMapAsVolume.GetConfigMapName(),
MountPath: configMapAsVolume.GetMountPath(),
}
podSpec.Volumes = append(podSpec.Volumes, configMapVolume)
podSpec.Containers[0].VolumeMounts = append(podSpec.Containers[0].VolumeMounts, configMapVolumeMount)
}

// Get config map env information
for _, configMapAsEnv := range kubernetesExecutorConfig.GetConfigMapAsEnv() {
for _, keyToEnv := range configMapAsEnv.GetKeyToEnv() {
configMapEnvVar := k8score.EnvVar{
Name: keyToEnv.GetEnvVar(),
ValueFrom: &k8score.EnvVarSource{
ConfigMapKeyRef: &k8score.ConfigMapKeySelector{
Key: keyToEnv.GetConfigMapKey(),
},
},
}
configMapEnvVar.ValueFrom.ConfigMapKeyRef.LocalObjectReference.Name = configMapAsEnv.GetConfigMapName()
podSpec.Containers[0].Env = append(podSpec.Containers[0].Env, configMapEnvVar)
}
}

// Get image pull secret information
for _, imagePullSecret := range kubernetesExecutorConfig.GetImagePullSecret() {
podSpec.ImagePullSecrets = append(podSpec.ImagePullSecrets, k8score.LocalObjectReference{Name: imagePullSecret.GetSecretName()})
Expand Down
117 changes: 117 additions & 0 deletions backend/src/v2/driver/driver_test.go
Expand Up @@ -606,6 +606,123 @@ func Test_extendPodSpecPatch_Secret(t *testing.T) {
}
}

func Test_extendPodSpecPatch_ConfigMap(t *testing.T) {
tests := []struct {
name string
k8sExecCfg *kubernetesplatform.KubernetesExecutorConfig
podSpec *k8score.PodSpec
expected *k8score.PodSpec
}{
{
"Valid - config map as volume",
&kubernetesplatform.KubernetesExecutorConfig{
ConfigMapAsVolume: []*kubernetesplatform.ConfigMapAsVolume{
{
ConfigMapName: "cm1",
MountPath: "/data/path",
},
},
},
&k8score.PodSpec{
Containers: []k8score.Container{
{
Name: "main",
},
},
},
&k8score.PodSpec{
Containers: []k8score.Container{
{
Name: "main",
VolumeMounts: []k8score.VolumeMount{
{
Name: "cm1",
MountPath: "/data/path",
},
},
},
},
Volumes: []k8score.Volume{
{
Name: "cm1",
VolumeSource: k8score.VolumeSource{
ConfigMap: &k8score.ConfigMapVolumeSource{
LocalObjectReference: k8score.LocalObjectReference{Name: "cm1"}},
},
},
},
},
},
{
"Valid - config map not specified",
&kubernetesplatform.KubernetesExecutorConfig{},
&k8score.PodSpec{
Containers: []k8score.Container{
{
Name: "main",
},
},
},
&k8score.PodSpec{
Containers: []k8score.Container{
{
Name: "main",
},
},
},
},
{
"Valid - config map as env",
&kubernetesplatform.KubernetesExecutorConfig{
ConfigMapAsEnv: []*kubernetesplatform.ConfigMapAsEnv{
{
ConfigMapName: "my-cm",
KeyToEnv: []*kubernetesplatform.ConfigMapAsEnv_ConfigMapKeyToEnvMap{
{
ConfigMapKey: "foo",
EnvVar: "CONFIG_MAP_VAR",
},
},
},
},
},
&k8score.PodSpec{
Containers: []k8score.Container{
{
Name: "main",
},
},
},
&k8score.PodSpec{
Containers: []k8score.Container{
{
Name: "main",
Env: []k8score.EnvVar{
{
Name: "CONFIG_MAP_VAR",
ValueFrom: &k8score.EnvVarSource{
ConfigMapKeyRef: &k8score.ConfigMapKeySelector{
k8score.LocalObjectReference{Name: "my-cm"},
"foo",
nil,
},
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := extendPodSpecPatch(tt.podSpec, tt.k8sExecCfg, nil, nil)
assert.Nil(t, err)
assert.Equal(t, tt.expected, tt.podSpec)
})
}
}

func Test_extendPodSpecPatch_ImagePullSecrets(t *testing.T) {
tests := []struct {
name string
Expand Down
2 changes: 1 addition & 1 deletion backend/third_party_licenses/apiserver.csv
Expand Up @@ -61,7 +61,7 @@ github.com/klauspost/cpuid,https://github.com/klauspost/cpuid/blob/v1.3.1/LICENS
github.com/klauspost/pgzip,https://github.com/klauspost/pgzip/blob/v1.2.5/LICENSE,MIT
github.com/kubeflow/pipelines/api/v2alpha1/go,https://github.com/kubeflow/pipelines/blob/758c91f76784/api/LICENSE,Apache-2.0
github.com/kubeflow/pipelines/backend,https://github.com/kubeflow/pipelines/blob/HEAD/LICENSE,Apache-2.0
github.com/kubeflow/pipelines/kubernetes_platform/go/kubernetesplatform,https://github.com/kubeflow/pipelines/blob/f51dc39614e4/kubernetes_platform/LICENSE,Apache-2.0
github.com/kubeflow/pipelines/kubernetes_platform/go/kubernetesplatform,https://github.com/kubeflow/pipelines/blob/3f0fc0629521/kubernetes_platform/LICENSE,Apache-2.0
github.com/kubeflow/pipelines/third_party/ml-metadata/go/ml_metadata,https://github.com/kubeflow/pipelines/blob/e1f0c010f800/third_party/ml-metadata/LICENSE,Apache-2.0
github.com/lann/builder,https://github.com/lann/builder/blob/47ae307949d0/LICENSE,MIT
github.com/lann/ps,https://github.com/lann/ps/blob/62de8c46ede0/LICENSE,MIT
Expand Down
2 changes: 1 addition & 1 deletion backend/third_party_licenses/driver.csv
Expand Up @@ -31,7 +31,7 @@ github.com/josharian/intern,https://github.com/josharian/intern/blob/v1.0.0/lice
github.com/json-iterator/go,https://github.com/json-iterator/go/blob/v1.1.12/LICENSE,MIT
github.com/kubeflow/pipelines/api/v2alpha1/go,https://github.com/kubeflow/pipelines/blob/758c91f76784/api/LICENSE,Apache-2.0
github.com/kubeflow/pipelines/backend,https://github.com/kubeflow/pipelines/blob/HEAD/LICENSE,Apache-2.0
github.com/kubeflow/pipelines/kubernetes_platform/go/kubernetesplatform,https://github.com/kubeflow/pipelines/blob/f51dc39614e4/kubernetes_platform/LICENSE,Apache-2.0
github.com/kubeflow/pipelines/kubernetes_platform/go/kubernetesplatform,https://github.com/kubeflow/pipelines/blob/3f0fc0629521/kubernetes_platform/LICENSE,Apache-2.0
github.com/kubeflow/pipelines/third_party/ml-metadata/go/ml_metadata,https://github.com/kubeflow/pipelines/blob/e1f0c010f800/third_party/ml-metadata/LICENSE,Apache-2.0
github.com/mailru/easyjson,https://github.com/mailru/easyjson/blob/v0.7.7/LICENSE,MIT
github.com/modern-go/concurrent,https://github.com/modern-go/concurrent/blob/bacd9c7ef1dd/LICENSE,Apache-2.0
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -31,7 +31,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/kubeflow/pipelines/api v0.0.0-20230331215358-758c91f76784
github.com/kubeflow/pipelines/kubernetes_platform v0.0.0-20240207171236-f51dc39614e4
github.com/kubeflow/pipelines/kubernetes_platform v0.0.0-20240213200615-3f0fc0629521
github.com/kubeflow/pipelines/third_party/ml-metadata v0.0.0-20230810215105-e1f0c010f800
github.com/lestrrat-go/strftime v1.0.4
github.com/mattn/go-sqlite3 v1.14.16
Expand Down
4 changes: 2 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 39 additions & 1 deletion kubernetes_platform/python/README.md
Expand Up @@ -57,6 +57,44 @@ def pipeline():
mount_path='/mnt/my_vol')
```

### ConfigMap: As environment variable
```python
from kfp import dsl
from kfp import kubernetes

@dsl.component
def print_config_map():
import os
print(os.environ['my-cm'])

@dsl.pipeline
def pipeline():
task = print_config_map()
kubernetes.use_config_map_as_env(task,
config_map_name='my-cm',
secret_key_to_env={'foo': 'CM_VAR'})
```

### ConfigMap: As mounted volume
```python
from kfp import dsl
from kfp import kubernetes

@dsl.component
def print_config_map():
with open('/mnt/my_vol') as f:
print(f.read())

@dsl.pipeline
def pipeline():
task = print_config_map()
kubernetes.use_secret_as_volume(task,
config_map_name='my-cm',
mount_path='/mnt/my_vol')
```



### PersistentVolumeClaim: Dynamically create PVC, mount, then delete
```python
from kfp import dsl
Expand Down Expand Up @@ -127,4 +165,4 @@ def my_pipeline():
annotation_key='run_id',
annotation_value='123456',
)
```
```
8 changes: 6 additions & 2 deletions kubernetes_platform/python/kfp/kubernetes/__init__.py
Expand Up @@ -24,11 +24,15 @@
'add_pod_label',
'add_pod_annotation',
'set_image_pull_secrets'
'use_config_map_as_env',
'use_config_map_as_volume',
]

from kfp.kubernetes.pod_metadata import add_pod_label
from kfp.kubernetes.pod_metadata import add_pod_annotation
from kfp.kubernetes.config_map import use_config_map_as_volume
from kfp.kubernetes.config_map import use_config_map_as_env
from kfp.kubernetes.node_selector import add_node_selector
from kfp.kubernetes.pod_metadata import add_pod_annotation
from kfp.kubernetes.pod_metadata import add_pod_label
from kfp.kubernetes.secret import use_secret_as_env
from kfp.kubernetes.secret import use_secret_as_volume
from kfp.kubernetes.volume import CreatePVC
Expand Down
87 changes: 87 additions & 0 deletions kubernetes_platform/python/kfp/kubernetes/config_map.py
@@ -0,0 +1,87 @@
# Copyright 2023 The Kubeflow Authors
roytman marked this conversation as resolved.
Show resolved Hide resolved
#
# 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 Dict

from google.protobuf import json_format
from kfp.dsl import PipelineTask
from kfp.kubernetes import common
from kfp.kubernetes import kubernetes_executor_config_pb2 as pb


def use_config_map_as_env(
task: PipelineTask,
config_map_name: str,
config_map_key_to_env: Dict[str, str],
) -> PipelineTask:
"""Use a Kubernetes ConfigMap as an environment variable as described in
https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#define-container-environment-variables-using-configmap-data.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we style the link using RST for clean rendering in the reference docs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, thanks


Args:
task: Pipeline task.
config_map_name: Name of the ConfigMap.
config_map_key_to_env: Dictionary of ConfigMap key to environment variable name. For example, ``{'foo': 'FOO'}`` sets the value of the ConfigMap's foo field to the environment variable ``FOO``.

Returns:
Task object with updated ConfigMap configuration.
"""

msg = common.get_existing_kubernetes_config_as_message(task)

key_to_env = [
pb.ConfigMapAsEnv.ConfigMapKeyToEnvMap(
config_map_key=config_map_key,
env_var=env_var,
) for config_map_key, env_var in config_map_key_to_env.items()
]
config_map_as_env = pb.ConfigMapAsEnv(
config_map_name=config_map_name,
key_to_env=key_to_env,
)

msg.config_map_as_env.append(config_map_as_env)

task.platform_config['kubernetes'] = json_format.MessageToDict(msg)

return task


def use_config_map_as_volume(
task: PipelineTask,
config_map_name: str,
mount_path: str,
) -> PipelineTask:
"""Use a Kubernetes ConfigMap by mounting its data to the task's container as
described by the `Kubernetes documentation <https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#add-configmap-data-to-a-volume>`_.

Args:
task: Pipeline task.
config_map_name: Name of the ConfigMap.
mount_path: Path to which to mount the ConfigMap data.

Returns:
Task object with updated ConfigMap configuration.
"""

msg = common.get_existing_kubernetes_config_as_message(task)

config_map_as_vol = pb.ConfigMapAsVolume(
config_map_name=config_map_name,
mount_path=mount_path,
)
msg.config_map_as_volume.append(config_map_as_vol)

task.platform_config['kubernetes'] = json_format.MessageToDict(msg)

return task
35 changes: 35 additions & 0 deletions kubernetes_platform/python/test/snapshot/data/config_map_as_env.py
@@ -0,0 +1,35 @@
# Copyright 2023 The Kubeflow Authors
#
# 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 kfp import dsl
from kfp import kubernetes


@dsl.component
def comp():
pass


@dsl.pipeline
def my_pipeline():
task = comp()
kubernetes.use_config_map_as_env(
task,
config_map_name='my-cm',
config_map_key_to_env={'foo': 'CONFIG_MAP_VAR'})


if __name__ == '__main__':
from kfp import compiler
compiler.Compiler().compile(my_pipeline, __file__.replace('.py', '.yaml'))