Skip to content

Commit 2182a71

Browse files
authored
fix: merge LaunchPlan security context per field on registration (#3442)
* fix: merge LaunchPlan security context per field on registration Signed-off-by: 1fanwang <1fannnw@gmail.com> * test: use docstring and top-level imports in security context merge test Signed-off-by: 1fanwang <1fannnw@gmail.com> --------- Signed-off-by: 1fanwang <1fannnw@gmail.com>
1 parent 6b24374 commit 2182a71

2 files changed

Lines changed: 57 additions & 8 deletions

File tree

flytekit/tools/translator.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from flytekit.models import common as _common_models
3131
from flytekit.models import interface as interface_models
3232
from flytekit.models import launch_plan as _launch_plan_models
33+
from flytekit.models import security as _security_models
3334
from flytekit.models.admin import workflow as admin_workflow_models
3435
from flytekit.models.admin.workflow import WorkflowSpec
3536
from flytekit.models.concurrency import ConcurrencyPolicy
@@ -317,6 +318,26 @@ def get_serializable_workflow(
317318
)
318319

319320

321+
def _merge_security_context(
322+
entity_sc: Optional[_security_models.SecurityContext],
323+
options_sc: Optional[_security_models.SecurityContext],
324+
) -> Optional[_security_models.SecurityContext]:
325+
"""Merge the launch plan's authored security context with the one supplied via registration options.
326+
327+
Registration options override the authored launch plan per field (not wholesale), so e.g. registering with
328+
only a service account does not drop secrets/tokens that were authored on the launch plan.
329+
"""
330+
if options_sc is None:
331+
return entity_sc
332+
if entity_sc is None:
333+
return options_sc
334+
return _security_models.SecurityContext(
335+
run_as=options_sc.run_as or entity_sc.run_as,
336+
secrets=options_sc.secrets or entity_sc.secrets,
337+
tokens=options_sc.tokens or entity_sc.tokens,
338+
)
339+
340+
320341
def get_serializable_launch_plan(
321342
entity_mapping: OrderedDict,
322343
settings: SerializationSettings,
@@ -379,7 +400,7 @@ def get_serializable_launch_plan(
379400
auth_role=None,
380401
raw_output_data_config=raw_prefix_config,
381402
max_parallelism=options.max_parallelism or entity.max_parallelism,
382-
security_context=options.security_context or entity.security_context,
403+
security_context=_merge_security_context(entity.security_context, options.security_context),
383404
overwrite_cache=options.overwrite_cache or entity.overwrite_cache,
384405
concurrency_policy=concurrency_policy,
385406
)

tests/flytekit/unit/core/test_serialization.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
1-
import re
1+
import dataclasses
22
import os
33
import typing
4-
import dataclasses
54
from collections import OrderedDict
65

76
import mock
87
import pytest
98

109
import flytekit.configuration
11-
from flytekit import ContainerTask, ImageSpec, kwtypes
10+
from flytekit import ContainerTask, ImageSpec, LaunchPlan, kwtypes
1211
from flytekit.configuration import Image, ImageConfig, SerializationSettings
12+
from flytekit.core.array_node_map_task import map_task
1313
from flytekit.core.condition import conditional
14+
from flytekit.core.dynamic_workflow_task import dynamic
15+
from flytekit.core.options import Options
1416
from flytekit.core.python_auto_container import get_registerable_container_image
1517
from flytekit.core.resources import Resources, ResourceSpec
16-
from flytekit.core.dynamic_workflow_task import dynamic
17-
from flytekit.core.array_node_map_task import map_task
1818
from flytekit.core.task import eager, task
1919
from flytekit.core.workflow import workflow
2020
from flytekit.exceptions.user import FlyteAssertion, FlyteMissingTypeException
2121
from flytekit.image_spec.image_spec import ImageBuildEngine
22+
from flytekit.models import task as task_models
2223
from flytekit.models.admin.workflow import WorkflowSpec
23-
from flytekit.models.annotation import TypeAnnotation
2424
from flytekit.models.literals import (
2525
BindingData,
2626
BindingDataCollection,
@@ -31,8 +31,8 @@
3131
Union,
3232
Void,
3333
)
34+
from flytekit.models.security import Identity, Secret, SecurityContext
3435
from flytekit.models.types import LiteralType, SimpleType, TypeStructure, UnionType
35-
from flytekit.models import task as task_models
3636
from flytekit.tools.translator import get_serializable
3737
from flytekit.types.error.error import FlyteError
3838

@@ -1272,3 +1272,31 @@ async def t1_eager(a: int) -> int:
12721272
],
12731273
limits=[]
12741274
)
1275+
1276+
1277+
def test_launch_plan_security_context_merge():
1278+
"""Registration options override the authored launch plan per field.
1279+
1280+
Registering with only a service account must not drop secrets/tokens that were
1281+
authored on the launch plan.
1282+
"""
1283+
1284+
@task
1285+
def t_sc() -> int:
1286+
return 1
1287+
1288+
@workflow
1289+
def wf_sc() -> int:
1290+
return t_sc()
1291+
1292+
lp = LaunchPlan.get_or_create(
1293+
workflow=wf_sc,
1294+
name="lp_sc_merge",
1295+
security_context=SecurityContext(secrets=[Secret(group="g", key="k")]),
1296+
)
1297+
opts = Options(security_context=SecurityContext(run_as=Identity(k8s_service_account="my-sa")))
1298+
1299+
serialized = get_serializable(OrderedDict(), serialization_settings, lp, options=opts)
1300+
sc = serialized.spec.security_context
1301+
assert sc.run_as.k8s_service_account == "my-sa"
1302+
assert [(s.group, s.key) for s in sc.secrets] == [("g", "k")]

0 commit comments

Comments
 (0)