|
| 1 | +import os |
| 2 | +import sys |
| 3 | +import tempfile |
1 | 4 | import webbrowser
|
2 | 5 | from collections.abc import Mapping
|
| 6 | +from contextlib import ExitStack |
3 | 7 | from pathlib import Path
|
| 8 | +from typing import Optional |
4 | 9 |
|
5 | 10 | import click
|
| 11 | +import jinja2 |
6 | 12 | from dagster_shared.plus.config import DagsterPlusCliConfig
|
7 | 13 | from dagster_shared.plus.login_server import start_login_server
|
8 | 14 |
|
9 | 15 | from dagster_dg.cli.shared_options import dg_global_options
|
| 16 | +from dagster_dg.cli.utils import create_temp_dagster_cloud_yaml_file |
10 | 17 | from dagster_dg.config import normalize_cli_config
|
11 | 18 | from dagster_dg.context import DgContext
|
12 | 19 | from dagster_dg.env import ProjectEnvVars
|
@@ -144,3 +151,130 @@ def pull_env_command(**global_options: object) -> None:
|
144 | 151 | click.echo(
|
145 | 152 | f"Environment variables not found for projects: {', '.join(projects_without_secrets)}"
|
146 | 153 | )
|
| 154 | + |
| 155 | + |
| 156 | +def _create_temp_deploy_dockerfile(dst_path, python_version): |
| 157 | + dockerfile_template_path = ( |
| 158 | + Path(__file__).parent.parent / "templates" / "deploy_uv_Dockerfile.jinja" |
| 159 | + ) |
| 160 | + |
| 161 | + loader = jinja2.FileSystemLoader(searchpath=os.path.dirname(dockerfile_template_path)) |
| 162 | + env = jinja2.Environment(loader=loader) |
| 163 | + |
| 164 | + template = env.get_template(os.path.basename(dockerfile_template_path)) |
| 165 | + |
| 166 | + with open(dst_path, "w", encoding="utf8") as f: |
| 167 | + f.write(template.render(python_version=python_version)) |
| 168 | + f.write("\n") |
| 169 | + |
| 170 | + |
| 171 | +@plus_group.command(name="deploy", cls=DgClickCommand) |
| 172 | +@click.option( |
| 173 | + "--organization", |
| 174 | + "organization", |
| 175 | + help="Dagster+ organization to which to deploy. If not set, defaults to the value set by `dg plus login`.", |
| 176 | + envvar="DAGSTER_PLUS_ORGANIZATION", |
| 177 | +) |
| 178 | +@click.option( |
| 179 | + "--deployment", |
| 180 | + "deployment", |
| 181 | + help="Name of the Dagster+ deployment to which to deploy. If not set, defaults to the value set by `dg plus login`.", |
| 182 | + envvar="DAGSTER_PLUS_DEPLOYMENT", |
| 183 | +) |
| 184 | +@click.option( |
| 185 | + "--python-version", |
| 186 | + "python_version", |
| 187 | + type=click.Choice(["3.9", "3.10", "3.11", "3.12"]), |
| 188 | + help=( |
| 189 | + "Python version used to deploy the project. If not set, defaults to the calling process's Python minor version." |
| 190 | + ), |
| 191 | +) |
| 192 | +@dg_global_options |
| 193 | +@cli_telemetry_wrapper |
| 194 | +def deploy_command( |
| 195 | + organization: Optional[str], |
| 196 | + deployment: Optional[str], |
| 197 | + python_version: Optional[str], |
| 198 | + **global_options: object, |
| 199 | +) -> None: |
| 200 | + """Deploy a project to Dagster Plus.""" |
| 201 | + cli_config = normalize_cli_config(global_options, click.get_current_context()) |
| 202 | + |
| 203 | + if not python_version: |
| 204 | + python_version = f"3.{sys.version_info.minor}" |
| 205 | + |
| 206 | + plus_config = DagsterPlusCliConfig.get() |
| 207 | + |
| 208 | + organization = organization or plus_config.organization |
| 209 | + if not organization: |
| 210 | + raise click.UsageError( |
| 211 | + "Organization not specified. To specify an organization, use the --organization option " |
| 212 | + "or run `dg plus login`." |
| 213 | + ) |
| 214 | + |
| 215 | + deployment = deployment or plus_config.default_deployment |
| 216 | + if not deployment: |
| 217 | + raise click.UsageError( |
| 218 | + "Deployment not specified. To specify a deployment, use the --deployment option " |
| 219 | + "or run `dg plus login`." |
| 220 | + ) |
| 221 | + |
| 222 | + # TODO This command should work in a workspace context too and apply to multiple projects |
| 223 | + dg_context = DgContext.for_project_environment(Path.cwd(), cli_config) |
| 224 | + |
| 225 | + # TODO Confirm that dagster-cloud is packaged in the project |
| 226 | + |
| 227 | + with ExitStack() as stack: |
| 228 | + # TODO Once this is split out into multiple commands, we need a default statedir |
| 229 | + # that can be persisted across commands. |
| 230 | + statedir = stack.enter_context(tempfile.TemporaryDirectory()) |
| 231 | + |
| 232 | + # Construct a dagster_cloud.yaml file based on info in the pyproject.toml |
| 233 | + dagster_cloud_yaml_file = stack.enter_context( |
| 234 | + create_temp_dagster_cloud_yaml_file(dg_context) |
| 235 | + ) |
| 236 | + |
| 237 | + dg_context.external_dagster_cloud_cli_command( |
| 238 | + [ |
| 239 | + "ci", |
| 240 | + "init", |
| 241 | + "--statedir", |
| 242 | + str(statedir), |
| 243 | + "--dagster-cloud-yaml-path", |
| 244 | + dagster_cloud_yaml_file, |
| 245 | + "--project-dir", |
| 246 | + str(dg_context.root_path), |
| 247 | + "--deployment", |
| 248 | + deployment, |
| 249 | + "--organization", |
| 250 | + organization, |
| 251 | + ], |
| 252 | + ) |
| 253 | + |
| 254 | + dockerfile_path = dg_context.root_path / "Dockerfile" |
| 255 | + if not os.path.exists(dockerfile_path): |
| 256 | + click.echo(f"No Dockerfile found - scaffolding a default one at {dockerfile_path}.") |
| 257 | + _create_temp_deploy_dockerfile(dockerfile_path, python_version) |
| 258 | + else: |
| 259 | + click.echo(f"Building using Dockerfile at {dockerfile_path}.") |
| 260 | + |
| 261 | + # TODO This command is serverless-specific, support hybrid as well |
| 262 | + dg_context.external_dagster_cloud_cli_command( |
| 263 | + [ |
| 264 | + "ci", |
| 265 | + "build", |
| 266 | + "--statedir", |
| 267 | + str(statedir), |
| 268 | + "--dockerfile-path", |
| 269 | + str(dg_context.root_path / "Dockerfile"), |
| 270 | + ], |
| 271 | + ) |
| 272 | + |
| 273 | + dg_context.external_dagster_cloud_cli_command( |
| 274 | + [ |
| 275 | + "ci", |
| 276 | + "deploy", |
| 277 | + "--statedir", |
| 278 | + str(statedir), |
| 279 | + ], |
| 280 | + ) |
0 commit comments