This is an automated email from the ASF dual-hosted git repository.
jason810496 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-4-test by this push:
new 65dad4f851c [v3-4-test] Run the lang-SDK native Dag tests on
Kubernetes (#75930) (#76008)
65dad4f851c is described below
commit 65dad4f851c8b3ba24501730b3cb8262d3a2ba6f
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 30 17:40:00 2026 +0800
[v3-4-test] Run the lang-SDK native Dag tests on Kubernetes (#75930)
(#76008)
* Name the native Dag k8s tests so the lang-SDK job selects them
The lang-SDK Kubernetes job runs `-k TestLangSdk`, which matched only
TestLangSdkMixedLanguageDag. The native Java and TypeScript classes were
deselected, so Kubernetes verified stub tasks only.
* Run the native Go Dag on Kubernetes
Go Dag parsing landed with ExecutableDagImporter, and the compose e2e covers
it, but the Kubernetes harness provisioned no Go native Dag bundle, so the
Go class was a skipped placeholder.
`breeze k8s setup-lang-sdk-test` now packs `go-sdk/example/native` into a
`lang-sdk-native-go` bucket, served as a Dag bundle. The single
ExecutableCoordinator already parses any executable bundle in every Dag
bundle, and the `golang` queue is already routed, so no coordinator config
changes.
* Serve the typescript_example handlers from ts-task-handlers
The native TypeScript Dag's trigger_downstream starts a typescript_example
run and waits for it, but that Dag's stub tasks could not start: the only
bundle in ts-task-handlers declares lang_sdk_mixed_language, so every task
failed to launch and the awaited run failed. The gap was invisible while
the native Dag tests were deselected.
---------
(cherry picked from commit 8401a190fa8174e8089914408fe82797a202f71b)
Co-authored-by: Jason(Zhe-You) Liu
<[email protected]>
---
.../airflow_breeze/commands/kubernetes_commands.py | 106 +++++++++++++++-----
.../tests/test_kubernetes_lang_sdk_commands.py | 44 +++++++--
kubernetes-tests/lang_sdk/README.md | 18 ++--
kubernetes-tests/lang_sdk/config/values.yaml | 9 +-
.../kubernetes_tests/test_lang_sdk_native_dag.py | 110 ++++++++++++++++++---
5 files changed, 232 insertions(+), 55 deletions(-)
diff --git a/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py
b/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py
index c980fcd1d13..dfc24ed0e0c 100644
--- a/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py
@@ -2548,6 +2548,9 @@ LANG_SDK_PATH = AIRFLOW_ROOT_PATH / "kubernetes-tests" /
"lang_sdk"
# ts_example is a standalone pnpm project that resolves the SDK through a
file: dependency.
LANG_SDK_GO_EXAMPLE_PATH = LANG_SDK_PATH / "go_example"
LANG_SDK_GO_BUNDLE_NAME = "lang_sdk_mixed_language"
+# The native Go Dag fixture the compose e2e also packs; a package of the
go-sdk module itself.
+LANG_SDK_GO_NATIVE_BUNDLE_PKG = "./example/native"
+LANG_SDK_GO_NATIVE_BUNDLE_NAME = "go_native_dags"
LANG_SDK_JAVA_EXAMPLE_PATH = LANG_SDK_PATH / "java_example"
# The reusable native Java Dag fixture the compose e2e also builds.
LANG_SDK_JAVA_NATIVE_BUNDLE_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" /
"java-native-bundle"
@@ -2574,6 +2577,9 @@ LANG_SDK_AIRFLOWIGNORE_ALL = LANG_SDK_PATH /
"airflowignore_all"
LANG_SDK_TS_EXAMPLE_PATH = AIRFLOW_ROOT_PATH / "ts-sdk" / "example"
LANG_SDK_TS_BUILDER_IMAGE = os.environ.get("NODE_BUILDER_IMAGE",
"node:22-alpine")
LANG_SDK_TS_BUNDLE_NAME = "bundle.min.mjs"
+# ts-sdk/example also holds the handlers of the Python-declared
typescript_example Dag, so it
+# goes into ts-task-handlers as well, under a name that does not clash with
the bundle there.
+LANG_SDK_TS_NATIVE_HANDLER_NAME = "native_bundle.min.mjs"
LANG_SDK_AWS_CONN_URI = (
"aws://test:test@/?region_name=us-east-1&"
"endpoint_url=http%3A%2F%2Flocalstack.airflow.svc.cluster.local%3A4566"
@@ -2679,7 +2685,12 @@ def _lang_sdk_fetch_upstream_sdk_sources(staging: Path,
output: Output | None) -
def _lang_sdk_build_go_bundle(
staging: Path, go_sdk_source: Path, output: Output | None, *, native: bool
= False
) -> None:
- """Build the Go bundle into ``staging/go-task-handlers`` and copy the
result into the staging dir.
+ """Pack the mixed-language Go bundle and the native Go Dag bundle into
their staging dirs.
+
+ ``go_example`` holds the handlers of the mixed-language Dag's stub tasks
+ (``staging/go-task-handlers``). ``go-sdk/example/native`` declares the
``go_native_*`` Dags with
+ no Python file, and is packed into ``staging/lang-sdk-native-go``, where
the Dag processor
+ parses it.
By default the build runs in an ephemeral Go toolchain container so the
host needs no Go install,
writing its caches into a gitignored dir under the repo. In ``native``
mode (used in CI, where the
@@ -2694,17 +2705,22 @@ def _lang_sdk_build_go_bundle(
"""
go_dir = staging / "go-task-handlers"
go_dir.mkdir(parents=True, exist_ok=True)
+ native_go_dir = staging / "lang-sdk-native-go"
+ native_go_dir.mkdir(parents=True, exist_ok=True)
example_rel = LANG_SDK_GO_EXAMPLE_PATH.relative_to(AIRFLOW_ROOT_PATH)
workspace = staging / "go_workspace"
example_path = workspace / example_rel
+ go_sdk_path = workspace / "go-sdk"
example_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(LANG_SDK_GO_EXAMPLE_PATH, example_path,
ignore=shutil.ignore_patterns(".home"))
# In dry-run the fetch/extract commands are skipped, so the extracted copy
and build outputs
# never materialize -- skip the filesystem work that depends on them.
if not get_dry_run():
- shutil.copytree(go_sdk_source, workspace / "go-sdk")
+ shutil.copytree(go_sdk_source, go_sdk_path)
output_bin = example_path / "bin" / LANG_SDK_GO_BUNDLE_NAME
output_bin.parent.mkdir(parents=True, exist_ok=True)
+ native_output_bin = go_sdk_path / "bin" / LANG_SDK_GO_NATIVE_BUNDLE_NAME
+ native_output_bin.parent.mkdir(parents=True, exist_ok=True)
# CGO_ENABLED=0 yields a fully static binary that runs on the stock
worker. The package built is
# the current dir (".") because go_example is its own module.
@@ -2725,6 +2741,20 @@ def _lang_sdk_build_go_bundle(
output=output,
check=True,
)
+ run_command(
+ [
+ "go",
+ "tool",
+ "airflow-go-pack",
+ "--output",
+ str(native_output_bin),
+ LANG_SDK_GO_NATIVE_BUNDLE_PKG,
+ ],
+ cwd=go_sdk_path,
+ env=go_env,
+ output=output,
+ check=True,
+ )
else:
uid_gid = f"{os.getuid()}:{os.getgid()}"
go_example_ctr = f"/repo/{example_rel.as_posix()}"
@@ -2733,30 +2763,33 @@ def _lang_sdk_build_go_bundle(
# the real go_example's gitignored cache dir so the caches persist
across scratch workspaces.
(LANG_SDK_GO_EXAMPLE_PATH / ".home").mkdir(parents=True, exist_ok=True)
get_console(output=output).print(f"[info]Building Go bundle in
{LANG_SDK_GO_BUILDER_IMAGE}")
- docker_base = [
- "docker",
- "run",
- "--rm",
- "--user",
- uid_gid,
- "-e",
- f"HOME={go_example_ctr}/.home",
- "-e",
- "USER=airflow",
- "-e",
- "CGO_ENABLED=0",
- "-v",
- f"{workspace}:/repo",
- "-v",
- f"{LANG_SDK_GO_EXAMPLE_PATH / '.home'}:{go_example_ctr}/.home",
- "-w",
- go_example_ctr,
- LANG_SDK_GO_BUILDER_IMAGE,
- ]
- run_command([*docker_base, "go", "mod", "tidy"], output=output,
check=True)
+
+ def docker_base(workdir: str) -> list[str]:
+ return [
+ "docker",
+ "run",
+ "--rm",
+ "--user",
+ uid_gid,
+ "-e",
+ f"HOME={go_example_ctr}/.home",
+ "-e",
+ "USER=airflow",
+ "-e",
+ "CGO_ENABLED=0",
+ "-v",
+ f"{workspace}:/repo",
+ "-v",
+ f"{LANG_SDK_GO_EXAMPLE_PATH / '.home'}:{go_example_ctr}/.home",
+ "-w",
+ workdir,
+ LANG_SDK_GO_BUILDER_IMAGE,
+ ]
+
+ run_command([*docker_base(go_example_ctr), "go", "mod", "tidy"],
output=output, check=True)
run_command(
[
- *docker_base,
+ *docker_base(go_example_ctr),
"go",
"tool",
"airflow-go-pack",
@@ -2767,8 +2800,22 @@ def _lang_sdk_build_go_bundle(
output=output,
check=True,
)
+ run_command(
+ [
+ *docker_base("/repo/go-sdk"),
+ "go",
+ "tool",
+ "airflow-go-pack",
+ "--output",
+ f"/repo/go-sdk/bin/{LANG_SDK_GO_NATIVE_BUNDLE_NAME}",
+ LANG_SDK_GO_NATIVE_BUNDLE_PKG,
+ ],
+ output=output,
+ check=True,
+ )
if not get_dry_run():
shutil.copy(output_bin, go_dir / LANG_SDK_GO_BUNDLE_NAME)
+ shutil.copy(native_output_bin, native_go_dir /
LANG_SDK_GO_NATIVE_BUNDLE_NAME)
def _lang_sdk_build_ts_bundles(staging: Path, output: Output | None, *,
native: bool = False) -> None:
@@ -3047,7 +3094,11 @@ def _lang_sdk_upload_artifacts(
``.airflowignore`` that excludes everything in it, so the Dag processor
never tries to
parse the packed Go bundle or the Java/TypeScript handler-only bundles as
Dags -- a
coordinator's own artifact lookup does not read ``.airflowignore``, so
stub tasks still
- find their handlers there. The two ``lang-sdk-native-*`` buckets carry no
such file.
+ find their handlers there. The three ``lang-sdk-native-*`` buckets carry
no such file.
+
+ ``ts-task-handlers`` holds two bundles: the mixed-language one and
``ts-sdk/example``, which
+ carries the handlers of ``typescript_example``. The coordinator picks
between them by the
+ dag_ids each declares, so the native Dag's ``trigger_downstream`` has a
run that can succeed.
"""
pod = run_command_with_k8s_env(
[
@@ -3070,6 +3121,7 @@ def _lang_sdk_upload_artifacts(
).stdout.strip()
go_bundle = staging / "go-task-handlers" / LANG_SDK_GO_BUNDLE_NAME
+ native_go_bundle = staging / "lang-sdk-native-go" /
LANG_SDK_GO_NATIVE_BUNDLE_NAME
native_ts_bundle = staging / "lang-sdk-native-ts" / LANG_SDK_TS_BUNDLE_NAME
handler_ts_bundle = staging / "ts-task-handlers" / LANG_SDK_TS_BUNDLE_NAME
if get_dry_run():
@@ -3096,12 +3148,14 @@ def _lang_sdk_upload_artifacts(
(downstream_dag, "/tmp/typescript_example.py"),
(native_java_jar, "/tmp/native_app.jar"),
(native_ts_bundle, "/tmp/native_bundle.min.mjs"),
+ (native_go_bundle, "/tmp/native_go_bundle"),
]
buckets = [
"go-task-handlers",
"java-task-handlers",
"ts-task-handlers",
"dags",
+ "lang-sdk-native-go",
"lang-sdk-native-java",
"lang-sdk-native-ts",
]
@@ -3116,6 +3170,8 @@ def _lang_sdk_upload_artifacts(
("/tmp/typescript_example.py", "s3://dags/typescript_example.py"),
("/tmp/native_app.jar",
f"s3://lang-sdk-native-java/{native_java_jar.name}"),
("/tmp/native_bundle.min.mjs",
f"s3://lang-sdk-native-ts/{LANG_SDK_TS_BUNDLE_NAME}"),
+ ("/tmp/native_bundle.min.mjs",
f"s3://ts-task-handlers/{LANG_SDK_TS_NATIVE_HANDLER_NAME}"),
+ ("/tmp/native_go_bundle",
f"s3://lang-sdk-native-go/{LANG_SDK_GO_NATIVE_BUNDLE_NAME}"),
]
for src, dest in copies:
diff --git a/dev/breeze/tests/test_kubernetes_lang_sdk_commands.py
b/dev/breeze/tests/test_kubernetes_lang_sdk_commands.py
index e1d10abd019..ea74987c0b6 100644
--- a/dev/breeze/tests/test_kubernetes_lang_sdk_commands.py
+++ b/dev/breeze/tests/test_kubernetes_lang_sdk_commands.py
@@ -53,8 +53,9 @@ def go_example(tmp_path, monkeypatch):
def upstream_go_sdk(tmp_path):
"""A fake extracted upstream-main go-sdk copy, distinguishable from
anything under go_example."""
sdk_dir = tmp_path / "upstream_go_sdk"
- sdk_dir.mkdir()
+ (sdk_dir / "bin").mkdir(parents=True)
(sdk_dir / "marker.go").write_text("upstream")
+ (sdk_dir / "bin" /
kubernetes_commands.LANG_SDK_GO_NATIVE_BUNDLE_NAME).write_text("binary")
return sdk_dir
@@ -95,27 +96,32 @@ class TestLangSdkBuildGoBundle:
def test_native_uses_host_go_toolchain(self, mock_run, tmp_path,
go_example, upstream_go_sdk):
_lang_sdk_build_go_bundle(tmp_path, upstream_go_sdk, None, native=True)
- cmd = mock_run.call_args.args[0]
+ tidy_call, handlers_call, native_call = mock_run.call_args_list
+ cmd = handlers_call.args[0]
assert cmd[:3] == ["go", "tool", "airflow-go-pack"]
assert "docker" not in cmd
- workspace_example = mock_run.call_args.kwargs["cwd"]
+ workspace_example = handlers_call.kwargs["cwd"]
# The build runs against a scratch workspace copy, not the real
go_example dir.
assert workspace_example != go_example
assert workspace_example.name == go_example.name
- assert mock_run.call_args.kwargs["env"]["CGO_ENABLED"] == "0"
+ assert handlers_call.kwargs["env"]["CGO_ENABLED"] == "0"
# The workspace mirrors the repo layout, with go-sdk swapped for the
upstream copy.
- assert (workspace_example.parent / "go-sdk" / "marker.go").read_text()
== "upstream"
+ workspace_go_sdk = workspace_example.parent / "go-sdk"
+ assert (workspace_go_sdk / "marker.go").read_text() == "upstream"
assert (tmp_path / "go-task-handlers" /
kubernetes_commands.LANG_SDK_GO_BUNDLE_NAME).exists()
# The scratch copy is re-tidied against the upstream go-sdk before
packing, in the same dir.
- tidy_call = mock_run.call_args_list[0]
assert tidy_call.args[0] == ["go", "mod", "tidy"]
assert tidy_call.kwargs["cwd"] == workspace_example
+ assert native_call.args[0][:3] == ["go", "tool", "airflow-go-pack"]
+ assert native_call.args[0][-1] ==
kubernetes_commands.LANG_SDK_GO_NATIVE_BUNDLE_PKG
+ assert native_call.kwargs["cwd"] == workspace_go_sdk
+ assert (tmp_path / "lang-sdk-native-go" /
kubernetes_commands.LANG_SDK_GO_NATIVE_BUNDLE_NAME).exists()
@mock.patch.object(kubernetes_commands, "run_command")
def test_container_mode_runs_in_docker(self, mock_run, tmp_path,
go_example, upstream_go_sdk):
_lang_sdk_build_go_bundle(tmp_path, upstream_go_sdk, None,
native=False)
- cmd = mock_run.call_args.args[0]
+ tidy_cmd, cmd, native_cmd = (call.args[0] for call in
mock_run.call_args_list)
assert cmd[0] == "docker"
assert kubernetes_commands.LANG_SDK_GO_BUILDER_IMAGE in cmd
# The workspace, not AIRFLOW_ROOT_PATH, is mounted at /repo; the
persistent HOME cache
@@ -126,10 +132,12 @@ class TestLangSdkBuildGoBundle:
home_mount = next(m for m in mounts if m.endswith("/.home"))
assert home_mount.startswith(str(go_example / ".home"))
# The scratch copy is re-tidied in the same container image before
packing.
- tidy_cmd = mock_run.call_args_list[0].args[0]
assert tidy_cmd[0] == "docker"
assert kubernetes_commands.LANG_SDK_GO_BUILDER_IMAGE in tidy_cmd
assert tidy_cmd[-3:] == ["go", "mod", "tidy"]
+ assert native_cmd[native_cmd.index("-w") + 1] == "/repo/go-sdk"
+ assert native_cmd[-1] ==
kubernetes_commands.LANG_SDK_GO_NATIVE_BUNDLE_PKG
+ assert (tmp_path / "lang-sdk-native-go" /
kubernetes_commands.LANG_SDK_GO_NATIVE_BUNDLE_NAME).exists()
@pytest.fixture
@@ -380,6 +388,9 @@ class TestLangSdkDryRun:
_lang_sdk_build_go_bundle(tmp_path, tmp_path /
"missing_upstream_go_sdk", None, native=True)
assert not (tmp_path / "go-task-handlers" /
kubernetes_commands.LANG_SDK_GO_BUNDLE_NAME).exists()
+ assert not (
+ tmp_path / "lang-sdk-native-go" /
kubernetes_commands.LANG_SDK_GO_NATIVE_BUNDLE_NAME
+ ).exists()
def test_build_java_jars_skips_jar_copies(
self, dry_run, tmp_path, java_example, java_native_bundle,
upstream_java_sdk
@@ -403,6 +414,23 @@ class TestLangSdkDryRun:
assert str(tmp_path / "lang-sdk-native-java" / "app.jar") in cp_sources
+class TestLangSdkUploadArtifacts:
+ @mock.patch.object(kubernetes_commands, "run_command_with_k8s_env")
+ def
test_native_ts_bundle_also_serves_the_typescript_example_handlers(self,
mock_run, dry_run, tmp_path):
+ """typescript_example's stub tasks look in ts-task-handlers, so the
bundle has to be there too."""
+ mock_run.return_value = mock.Mock(stdout="")
+
+ _lang_sdk_upload_artifacts(tmp_path, "3.11", "v1.35.0", None)
+
+ uploads = [
+ tuple(argv[-2:]) for call in mock_run.call_args_list if "awslocal"
in (argv := call.args[0])
+ ]
+ assert ("/tmp/native_bundle.min.mjs",
"s3://lang-sdk-native-ts/bundle.min.mjs") in uploads
+ assert ("/tmp/native_bundle.min.mjs",
"s3://ts-task-handlers/native_bundle.min.mjs") in uploads
+ # The mixed-language handlers keep their own name, so the two do not
overwrite each other.
+ assert ("/tmp/handler_bundle.min.mjs",
"s3://ts-task-handlers/bundle.min.mjs") in uploads
+
+
class TestSetupLangSdkTestNativeSelection:
@pytest.mark.parametrize(
("env_value", "expected_native"),
diff --git a/kubernetes-tests/lang_sdk/README.md
b/kubernetes-tests/lang_sdk/README.md
index 3ee89620669..a8be41a73cc 100644
--- a/kubernetes-tests/lang_sdk/README.md
+++ b/kubernetes-tests/lang_sdk/README.md
@@ -25,8 +25,11 @@ Two end-to-end tests exercise every lang-SDK coordinator
(Go, Java, TypeScript)
- **`test_lang_sdk_mixed_language.py`** runs one Dag mixing **Python + Go +
Java + TypeScript**
tasks, using the `extra.pod_template_file` routing in the `[sdk]
coordinators` config.
- **`test_lang_sdk_native_dag.py`** runs one Dag **declared entirely in each
language SDK** (no
- Python file at all): the Dag processor asks the packed Java jar / TypeScript
bundle to parse
- itself. Go has no native Dag parsing support yet, so its class is
unconditionally skipped.
+ Python file at all): the Dag processor asks the packed Go binary / Java jar
/ TypeScript bundle
+ to parse itself.
+
+Both files name their classes `TestLangSdk*`, because the CI job that
provisions this environment
+selects its tests with `-k TestLangSdk`.
Every coordinator is configured once, with no `[sdk]
dag_bundle_to_coordinator` entry needed:
with exactly one coordinator per language, there is nothing to disambiguate.
@@ -46,6 +49,7 @@ with exactly one coordinator per language, there is nothing
to disambiguate.
then marks the matched file
executable) → forks the
Go binary / Java jar /
TypeScript bundle
+ lang-sdk-native-go ── S3DagBundle ──► dag-processor runs the binary
(go-sdk coordinator)
lang-sdk-native-java ── S3DagBundle ──► dag-processor parses the jar
(java-sdk coordinator)
lang-sdk-native-ts ── S3DagBundle ──► dag-processor parses the bundle
(ts-sdk coordinator)
```
@@ -61,7 +65,7 @@ Key points:
that excludes everything in them, so the Dag processor never tries to parse
the packed Go bundle
or the Java/TypeScript handler-only bundles as Dags. A coordinator's own
artifact lookup does not
read `.airflowignore`, so stub tasks still find their handlers there.
-- `lang-sdk-native-java` and `lang-sdk-native-ts` carry no such file: parsing
them is the point.
+- The three `lang-sdk-native-*` buckets carry no such file: parsing them is
the point.
## Components
@@ -79,15 +83,17 @@ Key points:
The Go binary, Java jar, TypeScript bundle and the Dag files share one object
store (localstack)
but live in **separate buckets** (`go-task-handlers`, `java-task-handlers`,
`ts-task-handlers`,
-`dags`, `lang-sdk-native-java`, `lang-sdk-native-ts`).
+`dags`, `lang-sdk-native-go`, `lang-sdk-native-java`, `lang-sdk-native-ts`).
The native TypeScript bundle is `ts-sdk/example`, packed by `airflow-ts-pack`.
It carries both
halves of that example: the handlers for the Python-declared
`typescript_example` Dag, and the
natively declared `typescript_native_example`. `typescript_example.py` is
uploaded to the `dags`
bucket too, because the native Dag's trigger task starts a run of it and waits
for that run,
deferring to `DagStateTrigger` in the Python triggerer. The test un-pauses
`typescript_example`
-for that reason. The native Java Dag reuses
`airflow-e2e-tests/java-native-bundle`, the same fixture
-the compose e2e builds.
+for that reason, and the same bundle is uploaded to `ts-task-handlers` under a
second name, since
+that is where `typescript_example`'s own stub tasks look for their handlers.
The coordinator picks
+between the two bundles in that bucket by the dag_ids each declares. The
native Java Dag reuses `airflow-e2e-tests/java-native-bundle` and the native
+Go Dags reuse `go-sdk/example/native`, the same fixtures the compose e2e
builds.
## Which SDK sources get built
diff --git a/kubernetes-tests/lang_sdk/config/values.yaml
b/kubernetes-tests/lang_sdk/config/values.yaml
index 0e628696fa0..262f919d0e9 100644
--- a/kubernetes-tests/lang_sdk/config/values.yaml
+++ b/kubernetes-tests/lang_sdk/config/values.yaml
@@ -44,7 +44,7 @@ scheduler:
mountPath: /opt/airflow/lang_sdk/pod_templates
# Serve the baked-in example Dags (dags-folder), the lang-SDK stub Dag, every
-# coordinator's task-handler bundle, and the two native Dag bundles. This must
be
+# coordinator's task-handler bundle, and the three native Dag bundles. This
must be
# additive: the standard KubernetesExecutor tests share this run and trigger
# example Dags, so dropping dags-folder marks them stale and their runs 404.
# Configure it here (dagProcessor.dagBundleConfigList) rather than overriding
@@ -55,7 +55,7 @@ scheduler:
# that excludes every file in them, so the Dag processor never parses the
# packed Go bundle or the Java/TypeScript handler-only bundles as Dags; a
# coordinator's own artifact lookup does not read .airflowignore, so stub tasks
-# still find their handlers there. The two lang-sdk-native-* bundles carry no
+# still find their handlers there. The three lang-sdk-native-* bundles carry no
# such file: that is exactly what the Dag processor is meant to parse.
dagProcessor:
dagBundleConfigList:
@@ -82,6 +82,11 @@ dagProcessor:
kwargs:
bucket_name: "ts-task-handlers"
aws_conn_id: "aws_localstack"
+ - name: lang-sdk-native-go
+ classpath: "airflow.providers.amazon.aws.bundles.s3.S3DagBundle"
+ kwargs:
+ bucket_name: "lang-sdk-native-go"
+ aws_conn_id: "aws_localstack"
- name: lang-sdk-native-java
classpath: "airflow.providers.amazon.aws.bundles.s3.S3DagBundle"
kwargs:
diff --git
a/kubernetes-tests/tests/kubernetes_tests/test_lang_sdk_native_dag.py
b/kubernetes-tests/tests/kubernetes_tests/test_lang_sdk_native_dag.py
index 8f39649f42a..5af20530b6c 100644
--- a/kubernetes-tests/tests/kubernetes_tests/test_lang_sdk_native_dag.py
+++ b/kubernetes-tests/tests/kubernetes_tests/test_lang_sdk_native_dag.py
@@ -19,11 +19,14 @@ End-to-end tests of Dags declared entirely in a language
SDK (no Python file at
KubernetesExecutor.
The Dag processor parses each native Dag through the coordinator its language
routes to,
-from a Dag bundle holding only that language's artifact
(``lang-sdk-native-java`` /
-``lang-sdk-native-ts``). The same coordinators also serve the mixed-language
Dag's stub
-tasks (see ``test_lang_sdk_mixed_language.py``); a native task ignores
-``task_handler_bundle_name`` and reads its own Dag bundle instead, so one
coordinator per
-language is enough -- no ``[sdk] dag_bundle_to_coordinator`` is configured.
+from a Dag bundle holding only that language's artifact
(``lang-sdk-native-go`` /
+``lang-sdk-native-java`` / ``lang-sdk-native-ts``). The same coordinators also
serve the
+mixed-language Dag's stub tasks (see ``test_lang_sdk_mixed_language.py``); a
native task
+ignores ``task_handler_bundle_name`` and reads its own Dag bundle instead, so
one coordinator
+per language is enough -- no ``[sdk] dag_bundle_to_coordinator`` is configured.
+
+The class names all start with ``TestLangSdk`` because the CI job that
provisions this
+environment selects its tests with ``-k TestLangSdk``.
Prerequisites are provisioned by ``breeze k8s setup-lang-sdk-test``.
"""
@@ -45,18 +48,97 @@ _skip_unless_lang_sdk = pytest.mark.skipif(
)
[email protected](
- reason="Native Go Dag parsing is not supported yet: there is no Go Dag
importer, and "
- "ExecutableCoordinator has no parse-Dag command. Unskip once a Go bundle
can answer a "
- "Dag-parse request."
-)
-class TestNativeGoDagOnKubernetes(BaseK8STest):
+@_skip_unless_lang_sdk
+class TestLangSdkNativeGoDag(BaseK8STest):
+ """Declared entirely in Go; the same ``go-sdk/example/native`` the compose
e2e packs.
+
+ Covers the path a Dag with no Python file takes: the Dag processor runs
the packed
+ ``go_native_dags`` binary through the Executable coordinator to parse it,
the scheduler reads
+ the serialized Dag, and each task runs in its own pod on the ``golang``
queue the Dag sets.
+ The graph has nested task groups, a fan-in, an ``If``, a ``Switch`` and a
trigger-rule join, so
+ pod-per-task scheduling is exercised against branches that skip rather
than a linear chain.
+ """
+
+ DAG_ID = "go_native_pipeline"
+ TASK_IDS = [
+ "seed",
+ "extract.regions.north",
+ "extract.regions.south",
+ "extract.summarize",
+ "anyRows",
+ "loadRows",
+ "pick",
+ "publishWeekly",
+ "cleanup",
+ ]
+ # The Else side of the If, and the Case the Switch does not choose.
+ SKIPPED_TASK_IDS = ["reportEmpty", "publishDaily"]
+
+ def _ensure_variable(self, key: str, value: str) -> None:
+ resp = self.session.post(f"http://{self.host}/variables", json={"key":
key, "value": value})
+ assert resp.status_code in (200, 201, 409), f"Could not create
variable {key}: {resp.text}"
+
+ def _ensure_connection(self) -> None:
+ """Create the ``test_http`` Connection that ``seed`` reads, as the
compose e2e defines it."""
+ resp = self.session.post(
+ f"http://{self.host}/connections",
+ json={
+ "connection_id": "test_http",
+ "conn_type": "http",
+ "login": "user",
+ "password": "pass",
+ "host": "example.com",
+ "port": 1234,
+ "extra": '{"param1": "val1", "param2": "val2"}',
+ },
+ )
+ assert resp.status_code in (200, 201, 409), f"Could not create
connection test_http: {resp.text}"
+
+ @pytest.mark.execution_timeout(900)
def test_native_go_dag_succeeds(self):
- pass
+ # The cadence names the case the Switch chooses, so `publishDaily` is
the side that skips.
+ # Both regions are non-empty literals, so the If takes its Then branch
and `reportEmpty`
+ # is the side that skips.
+ self._ensure_variable("go_native_greeting", "hello from the k8s test")
+ self._ensure_variable("go_native_cadence", "weekly")
+ self._ensure_connection()
+
+ dag_run_id, logical_date = self.start_job_in_kubernetes(self.DAG_ID,
self.host)
+ print(f"Triggered {self.DAG_ID} run {dag_run_id}
(logical_date={logical_date})")
+
+ for task_id in self.TASK_IDS:
+ self.monitor_task(
+ host=self.host,
+ dag_run_id=dag_run_id,
+ dag_id=self.DAG_ID,
+ task_id=task_id,
+ expected_final_state="success",
+ timeout=_TIMEOUT,
+ )
+
+ # The branch that was not taken is skipped, not failed, which is what
proves the skip
+ # reached the supervisor rather than the task erroring.
+ for skipped in self.SKIPPED_TASK_IDS:
+ self.monitor_task(
+ host=self.host,
+ dag_run_id=dag_run_id,
+ dag_id=self.DAG_ID,
+ task_id=skipped,
+ expected_final_state="skipped",
+ timeout=_TIMEOUT,
+ )
+
+ self.ensure_dag_expected_state(
+ host=self.host,
+ logical_date=logical_date,
+ dag_id=self.DAG_ID,
+ expected_final_state="success",
+ timeout=_TIMEOUT,
+ )
@_skip_unless_lang_sdk
-class TestNativeJavaDagOnKubernetes(BaseK8STest):
+class TestLangSdkNativeJavaDag(BaseK8STest):
"""Declared entirely in Java; the interface API half of
``airflow-e2e-tests/java-native-bundle``.
Covers the path a Dag with no Python file takes: the Dag processor parses
the
@@ -127,7 +209,7 @@ class TestNativeJavaDagOnKubernetes(BaseK8STest):
@_skip_unless_lang_sdk
-class TestNativeTypeScriptDagOnKubernetes(BaseK8STest):
+class TestLangSdkNativeTypeScriptDag(BaseK8STest):
"""The same Dag the SDK and Airflow e2e suites run, under the Node
coordinator.
Covers the path a Dag with no Python file takes: the Dag processor parses
the