Skip to content

Commit 0d82572

Browse files
authored
feat: support multi-container cloud run jobs (#259)
* feat: support multi-container cloud run jobs * docs: changelog
1 parent a165cf1 commit 0d82572

5 files changed

Lines changed: 125 additions & 1 deletion

File tree

libraries/dagster-contrib-gcp/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 0.0.5
4+
5+
### Updated
6+
7+
- (pull/259) Added the `container_name` field option to support multi-container job launching
8+
39
## 0.0.4
410

511
### Updated

libraries/dagster-contrib-gcp/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ my-code-location-1:
6767
name: my-cloud-run-job-1
6868
project_id: gcp_123
6969
region: us-central1
70+
71+
# Multi-container support - which container to override on run
72+
my-code-location-1:
73+
name: my-cloud-run-job-1
74+
project_id: gcp_123
75+
region: us-central1
76+
container_name: my-dagster-container-name
7077
```
7178
7279
Additional steps may be required for configuring IAM permissions, etc. In particular:

libraries/dagster-contrib-gcp/dagster_contrib_gcp/cloud_run/run_launcher.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ def launch_run(self, context: LaunchRunContext) -> None:
9898
context.dagster_run.run_id, {"cloud_run_job_execution_id": execution_id}
9999
)
100100

101+
def get_container_name_for_code_location_or_default(
102+
self, job_config: dict[str, Any]
103+
) -> str | None:
104+
"""Returns the specific container_name for multi-container cloud run jobs to override"""
105+
job_config = check.dict_param(job_config, "job_config")
106+
return job_config.get("container_name", None)
107+
101108
def get_project_for_code_location_or_default(
102109
self, job_config: dict[str, Any]
103110
) -> str:
@@ -132,6 +139,19 @@ def fully_qualified_job_name(self, code_location_name: str) -> str:
132139
f"projects/{project_id_for_job}/locations/{region_for_job}/jobs/{job_name}"
133140
)
134141

142+
def specific_container_name(self, code_location_name: str) -> str | None:
143+
try:
144+
job = self.job_name_by_code_location[code_location_name]
145+
except KeyError:
146+
raise Exception(
147+
f"No run launcher defined for code location: {code_location_name}"
148+
)
149+
# if not specific, then we default to legacy 1 container behavior (no name)
150+
if isinstance(job, str):
151+
return None
152+
153+
return self.get_container_name_for_code_location_or_default(job)
154+
135155
def resolve_secret(self, secret_name: str) -> Any:
136156
client = SecretManagerServiceClient()
137157
latest = AccessSecretVersionRequest(name=secret_name)
@@ -185,13 +205,17 @@ def env_override_for_code_location(
185205
def create_execution(self, code_location_name: str, args: Sequence[str]):
186206
job_name = self.fully_qualified_job_name(code_location_name)
187207
job_env = self.env_override_for_code_location(code_location_name)
188-
return self.execute_job(job_name, args=args, env=job_env)
208+
container_name = self.specific_container_name(code_location_name)
209+
return self.execute_job(
210+
job_name, args=args, env=job_env, container_name=container_name
211+
)
189212

190213
def execute_job(
191214
self,
192215
fully_qualified_job_name: str,
193216
args: Sequence[str] | None = None,
194217
env: Optional["dict[str, str]"] = None,
218+
container_name: str | None = None,
195219
) -> Operation:
196220
request = RunJobRequest(name=fully_qualified_job_name)
197221

@@ -202,6 +226,8 @@ def execute_job(
202226
overrides["env"] = [
203227
k8s_min.EnvVar(name=name, value=value) for name, value in env.items()
204228
]
229+
if container_name:
230+
overrides["name"] = container_name
205231

206232
container_overrides = [RunJobRequest.Overrides.ContainerOverride(**overrides)]
207233

@@ -280,6 +306,8 @@ def config_type(cls) -> "UserConfigSchema":
280306
" pair where the key is the code location name and the value is the job name. "
281307
"Optionally, each code location key may specifiy the `job_name` and `project_id` "
282308
"override value in order to the code location to a different GCP project ID."
309+
"If using multi-container jobs, you may also specify `container_name` which"
310+
"will be used to override the specific container"
283311
),
284312
),
285313
"run_job_retry": Field(

libraries/dagster-contrib-gcp/dagster_contrib_gcp_tests/cloud_run_tests/conftest.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,29 @@ def instance_with_job_configs(
7373
yield dagster_instance
7474

7575

76+
@pytest.fixture
77+
def instance_with_multicontainer_job_configs(
78+
instance_cm: Callable[..., ContextManager[DagsterInstance]],
79+
) -> Iterator[DagsterInstance]:
80+
with instance_cm(
81+
{
82+
"project": "test_project",
83+
"region": "test_region",
84+
"job_name_by_code_location": {
85+
IN_PROCESS_NAME: {
86+
"name": "test_job_with_config",
87+
"project_id": "test_gcp-123",
88+
"region": "other_test_region",
89+
"container_name": "specific_container",
90+
}
91+
},
92+
"run_job_retry": {"wait": 1, "timeout": 60},
93+
"run_timeout": 7200,
94+
}
95+
) as dagster_instance:
96+
yield dagster_instance
97+
98+
7699
@pytest.fixture
77100
def workspace(instance: DagsterInstance) -> Iterator[WorkspaceRequestContext]:
78101
with in_process_test_workspace(
@@ -101,6 +124,21 @@ def workspace_with_job_configs(
101124
yield workspace
102125

103126

127+
@pytest.fixture
128+
def workspace_with_multicontainer_job_configs(
129+
instance_with_multicontainer_job_configs: DagsterInstance,
130+
) -> Iterator[WorkspaceRequestContext]:
131+
with in_process_test_workspace(
132+
instance_with_multicontainer_job_configs,
133+
loadable_target_origin=LoadableTargetOrigin(
134+
python_file=repo.__file__,
135+
attribute=repo.repository.name,
136+
),
137+
container_image="dagster:latest",
138+
) as workspace:
139+
yield workspace
140+
141+
104142
@pytest.fixture
105143
def job() -> JobDefinition:
106144
return repo.job
@@ -135,6 +173,19 @@ def run_with_job_configs(
135173
)
136174

137175

176+
@pytest.fixture
177+
def run_with_multicontainer_job_configs(
178+
instance_with_multicontainer_job_configs: DagsterInstance,
179+
job: JobDefinition,
180+
external_job: RemoteJob,
181+
) -> DagsterRun:
182+
return instance_with_multicontainer_job_configs.create_run_for_job(
183+
job,
184+
remote_job_origin=external_job.get_remote_origin(),
185+
job_code_origin=external_job.get_python_origin(),
186+
)
187+
188+
138189
@pytest.fixture
139190
def executions():
140191
return {}

libraries/dagster-contrib-gcp/dagster_contrib_gcp_tests/cloud_run_tests/test_run_launcher.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,38 @@ def test_launch_run_with_job_config(
6868
assert args[0].overrides.timeout == dt.timedelta(seconds=7200)
6969

7070

71+
def test_launch_run_with_multicontainer_job_config(
72+
instance_with_multicontainer_job_configs: DagsterInstance,
73+
run_with_multicontainer_job_configs: DagsterRun,
74+
workspace_with_multicontainer_job_configs: BaseWorkspaceRequestContext,
75+
mock_jobs_client,
76+
mock_executions_client,
77+
):
78+
instance_with_multicontainer_job_configs.launch_run(
79+
run_with_multicontainer_job_configs.run_id,
80+
workspace_with_multicontainer_job_configs,
81+
)
82+
run = check.not_none(
83+
instance_with_multicontainer_job_configs.get_run_by_id(
84+
run_with_multicontainer_job_configs.run_id
85+
)
86+
)
87+
88+
# Assert the correct tag is set
89+
assert run.tags["cloud_run_job_execution_id"] == "test_execution_id"
90+
91+
# Check that mock was called with correct args
92+
mock_jobs_client.run_job.assert_called_once()
93+
args, _ = mock_jobs_client.run_job.call_args
94+
assert isinstance(args[0], RunJobRequest)
95+
assert (
96+
args[0].name
97+
== "projects/test_gcp-123/locations/other_test_region/jobs/test_job_with_config"
98+
)
99+
assert args[0].overrides.timeout == dt.timedelta(seconds=7200)
100+
assert args[0].overrides.container_overrides[0].name == "specific_container"
101+
102+
71103
@patch.object(run_launcher.CloudRunRunLauncher, "resolve_secret")
72104
def test_env_override_for_code_location(patched_resolve_secret):
73105
mock = MagicMock()

0 commit comments

Comments
 (0)