dagster-cloud-cli 1.12.16__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. dagster_cloud_cli/__init__.py +5 -0
  2. dagster_cloud_cli/commands/__init__.py +0 -0
  3. dagster_cloud_cli/commands/branch_deployment/__init__.py +154 -0
  4. dagster_cloud_cli/commands/ci/__init__.py +1207 -0
  5. dagster_cloud_cli/commands/ci/checks.py +135 -0
  6. dagster_cloud_cli/commands/ci/report.py +49 -0
  7. dagster_cloud_cli/commands/ci/state.py +129 -0
  8. dagster_cloud_cli/commands/ci/utils.py +22 -0
  9. dagster_cloud_cli/commands/config.py +250 -0
  10. dagster_cloud_cli/commands/deployment/__init__.py +108 -0
  11. dagster_cloud_cli/commands/deployment/alert_policies/__init__.py +0 -0
  12. dagster_cloud_cli/commands/deployment/alert_policies/commands.py +52 -0
  13. dagster_cloud_cli/commands/integration/__init__.py +6 -0
  14. dagster_cloud_cli/commands/integration/atlan/__init__.py +94 -0
  15. dagster_cloud_cli/commands/job/__init__.py +117 -0
  16. dagster_cloud_cli/commands/metrics.py +114 -0
  17. dagster_cloud_cli/commands/organization/__init__.py +134 -0
  18. dagster_cloud_cli/commands/organization/saml/__init__.py +0 -0
  19. dagster_cloud_cli/commands/organization/saml/commands.py +85 -0
  20. dagster_cloud_cli/commands/run/__init__.py +27 -0
  21. dagster_cloud_cli/commands/serverless/Dockerfile +38 -0
  22. dagster_cloud_cli/commands/serverless/Dockerfile.dagster-cloud-cli +20 -0
  23. dagster_cloud_cli/commands/serverless/__init__.py +560 -0
  24. dagster_cloud_cli/commands/workspace/__init__.py +358 -0
  25. dagster_cloud_cli/config/__init__.py +6 -0
  26. dagster_cloud_cli/config/jinja_template_loader.py +45 -0
  27. dagster_cloud_cli/config/list_merge_strategies.py +77 -0
  28. dagster_cloud_cli/config/loader.py +23 -0
  29. dagster_cloud_cli/config/merger.py +92 -0
  30. dagster_cloud_cli/config/models.py +76 -0
  31. dagster_cloud_cli/config_utils.py +506 -0
  32. dagster_cloud_cli/core/__init__.py +0 -0
  33. dagster_cloud_cli/core/agent_queue.py +3 -0
  34. dagster_cloud_cli/core/alert_types.py +26 -0
  35. dagster_cloud_cli/core/artifacts.py +192 -0
  36. dagster_cloud_cli/core/docker_runner.py +57 -0
  37. dagster_cloud_cli/core/errors.py +32 -0
  38. dagster_cloud_cli/core/graphql_client.py +420 -0
  39. dagster_cloud_cli/core/headers/__init__.py +0 -0
  40. dagster_cloud_cli/core/headers/auth.py +16 -0
  41. dagster_cloud_cli/core/headers/impl.py +31 -0
  42. dagster_cloud_cli/core/headers/versioning/__init__.py +0 -0
  43. dagster_cloud_cli/core/headers/versioning/constants.py +2 -0
  44. dagster_cloud_cli/core/pex_builder/__init__.py +8 -0
  45. dagster_cloud_cli/core/pex_builder/code_location.py +126 -0
  46. dagster_cloud_cli/core/pex_builder/deploy.py +255 -0
  47. dagster_cloud_cli/core/pex_builder/deps.py +495 -0
  48. dagster_cloud_cli/core/pex_builder/git_context.py +29 -0
  49. dagster_cloud_cli/core/pex_builder/github_context.py +178 -0
  50. dagster_cloud_cli/core/pex_builder/gitlab_context.py +30 -0
  51. dagster_cloud_cli/core/pex_builder/parse_workspace.py +39 -0
  52. dagster_cloud_cli/core/pex_builder/pex_registry.py +148 -0
  53. dagster_cloud_cli/core/pex_builder/platforms.py +6105 -0
  54. dagster_cloud_cli/core/pex_builder/selftest.py +14 -0
  55. dagster_cloud_cli/core/pex_builder/source.py +166 -0
  56. dagster_cloud_cli/core/pex_builder/util.py +165 -0
  57. dagster_cloud_cli/core/workspace.py +145 -0
  58. dagster_cloud_cli/docker_utils.py +98 -0
  59. dagster_cloud_cli/entrypoint.py +209 -0
  60. dagster_cloud_cli/gql.py +1025 -0
  61. dagster_cloud_cli/pex_utils.py +180 -0
  62. dagster_cloud_cli/py.typed +0 -0
  63. dagster_cloud_cli/types.py +78 -0
  64. dagster_cloud_cli/ui.py +101 -0
  65. dagster_cloud_cli/utils.py +107 -0
  66. dagster_cloud_cli/version.py +1 -0
  67. dagster_cloud_cli-1.12.16.dist-info/METADATA +31 -0
  68. dagster_cloud_cli-1.12.16.dist-info/RECORD +71 -0
  69. dagster_cloud_cli-1.12.16.dist-info/WHEEL +5 -0
  70. dagster_cloud_cli-1.12.16.dist-info/entry_points.txt +3 -0
  71. dagster_cloud_cli-1.12.16.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,14 @@
1
+ # Simple environment test script
2
+
3
+ import os
4
+
5
+ from dagster_cloud_cli.core.pex_builder import util
6
+
7
+ if __name__ == "__main__":
8
+ pex_environ = {name: value for name, value in os.environ.items() if name.startswith("PEX")}
9
+
10
+ proc = util.run_pex_command(["--version"])
11
+
12
+ proc = util.run_dagster_cloud_cli_command(["--version"])
13
+
14
+ proc = util.run_dagster_command(["--version"])
@@ -0,0 +1,166 @@
1
+ # Build source.pex, given a project root
2
+
3
+ import os
4
+ import os.path
5
+ import shutil
6
+ import subprocess
7
+ import tempfile
8
+ from uuid import uuid4
9
+
10
+ import click
11
+
12
+ from dagster_cloud_cli import ui
13
+ from dagster_cloud_cli.core.pex_builder import deps, util
14
+
15
+ # inspired by https://github.com/github/gitignore/blob/main/Python.gitignore
16
+ # would be nice to just read the gitignore and use that
17
+ IGNORED_PATTERNS = [
18
+ ".git",
19
+ "__pycache__",
20
+ ".pytest_cache",
21
+ ".tox",
22
+ ".nox",
23
+ "*.pyc",
24
+ ".mypy_cache",
25
+ ".cache",
26
+ ".venv",
27
+ ]
28
+
29
+
30
+ def build_source_pex(
31
+ code_directory: str, local_package_paths: list[str], output_directory, python_version
32
+ ):
33
+ output_directory = os.path.abspath(output_directory)
34
+ os.makedirs(output_directory, exist_ok=True)
35
+ code_directory = os.path.abspath(code_directory)
36
+ tmp_pex_path = os.path.join(output_directory, f"source-tmp-{uuid4()}.pex")
37
+
38
+ build_pex_using_setup_py(code_directory, local_package_paths, tmp_pex_path, python_version)
39
+
40
+ pex_info = util.get_pex_info(tmp_pex_path)
41
+ pex_hash = pex_info["pex_hash"]
42
+ pex_name = f"source-{pex_hash}.pex"
43
+ final_pex_path = os.path.join(output_directory, pex_name)
44
+ os.rename(tmp_pex_path, final_pex_path)
45
+ ui.print(f"Wrote source pex: {final_pex_path}")
46
+ return final_pex_path
47
+
48
+
49
+ def _build_local_package(local_dir: str, build_dir: str, python_interpreter: str):
50
+ if os.path.exists(os.path.join(local_dir, "setup.py")):
51
+ ui.print(f"Building package at {local_dir!r} using {python_interpreter} setup.py build")
52
+ command = [
53
+ python_interpreter,
54
+ "setup.py",
55
+ "build",
56
+ "--build-lib",
57
+ build_dir,
58
+ ]
59
+ subprocess.run(command, check=True, cwd=local_dir)
60
+ elif os.path.exists(os.path.join(local_dir, "pyproject.toml")):
61
+ ui.print(f"Building package at {local_dir!r} using {python_interpreter} -m pip install")
62
+ # Use pip install with --target to build and install the package to the build directory
63
+ # This handles pyproject.toml files properly using modern Python build standards
64
+ command = [
65
+ python_interpreter,
66
+ "-m",
67
+ "pip",
68
+ "install",
69
+ "--target",
70
+ build_dir,
71
+ "--no-deps", # Don't install dependencies, just the package itself
72
+ ".",
73
+ ]
74
+ subprocess.run(command, check=True, cwd=local_dir)
75
+ else:
76
+ ui.warn(f"No setup.py or pyproject.toml found in {local_dir!r} - will not build.")
77
+
78
+
79
+ def build_pex_using_setup_py(
80
+ code_directory: str,
81
+ local_package_paths: list[str],
82
+ tmp_pex_path,
83
+ python_version,
84
+ ):
85
+ """Builds package using setup.py and copies built output into PEX."""
86
+ python_interpreter = util.python_interpreter_for(python_version)
87
+ with tempfile.TemporaryDirectory() as build_dir, tempfile.TemporaryDirectory() as sources_dir:
88
+ included_dirs = [build_dir]
89
+
90
+ for local_dir in local_package_paths:
91
+ _build_local_package(
92
+ local_dir=local_dir,
93
+ build_dir=build_dir,
94
+ python_interpreter=python_interpreter,
95
+ )
96
+
97
+ _build_local_package(
98
+ local_dir=code_directory,
99
+ build_dir=build_dir,
100
+ python_interpreter=python_interpreter,
101
+ )
102
+
103
+ # We always include the code_directory source in a special package called working_directory
104
+ ui.print(
105
+ f"Bundling the source {code_directory} into the 'working_directory' package at {sources_dir}",
106
+ )
107
+ _prepare_working_directory(code_directory, sources_dir)
108
+ included_dirs.append(sources_dir)
109
+
110
+ # note this may include tests directories
111
+ proc = util.build_pex(
112
+ included_dirs,
113
+ requirements_filepaths=[],
114
+ pex_flags=util.get_pex_flags(python_version),
115
+ output_pex_path=tmp_pex_path,
116
+ )
117
+
118
+ if proc.returncode:
119
+ ui.error("build source pex stdout:")
120
+ ui.error(util.indent(proc.stdout.decode("utf-8")))
121
+ ui.error("build source pex stderr:")
122
+ ui.error(util.indent(proc.stderr.decode("utf-8")))
123
+ raise ui.error(
124
+ f"Failed to package source code into a source pex, error code: {proc.returncode}"
125
+ )
126
+ ui.print(f"Built source pex: {tmp_pex_path}")
127
+
128
+
129
+ def _prepare_working_directory(code_directory, sources_directory):
130
+ # Copy code_directory contents into a package called working_directory under sources_directory
131
+ package_dir = os.path.join(sources_directory, "working_directory")
132
+ os.makedirs(package_dir, exist_ok=True)
133
+
134
+ with open(os.path.join(package_dir, "__init__.py"), "w", encoding="utf-8") as init_file:
135
+ init_file.write("# Auto generated package containing the original source at root/")
136
+
137
+ shutil.copytree(
138
+ code_directory,
139
+ os.path.join(package_dir, "root"),
140
+ dirs_exist_ok=True,
141
+ ignore=shutil.ignore_patterns(*IGNORED_PATTERNS),
142
+ # We copy over symlinks as symlinks rather than try and resolve them and copy the target.
143
+ # This prevents build failures if the target file doesn't exist locally. If the target
144
+ # doesn't exist at runtime, this will fail and that is acceptable.
145
+ symlinks=True,
146
+ )
147
+
148
+
149
+ @click.command()
150
+ @click.argument("project_dir", type=click.Path(exists=True))
151
+ @click.argument("build_output_dir", type=click.Path(exists=False))
152
+ @util.python_version_option()
153
+ def source_main(project_dir, build_output_dir, python_version):
154
+ local_packages, _ = deps.get_deps_requirements(
155
+ code_directory=project_dir, python_version=python_version
156
+ )
157
+ build_source_pex(
158
+ project_dir,
159
+ local_packages.local_package_paths,
160
+ build_output_dir,
161
+ util.parse_python_version(python_version),
162
+ )
163
+
164
+
165
+ if __name__ == "__main__":
166
+ source_main()
@@ -0,0 +1,165 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ from subprocess import CompletedProcess
7
+ from zipfile import ZipFile
8
+
9
+ import click
10
+ from packaging import version
11
+
12
+ from dagster_cloud_cli.core.pex_builder.platforms import COMPLETE_PLATFORMS
13
+ from dagster_cloud_cli.utils import DEFAULT_PYTHON_VERSION, SUPPORTED_PYTHON_VERSIONS
14
+
15
+ TARGET_PYTHON_VERSIONS = [
16
+ version.Version(python_version) for python_version in SUPPORTED_PYTHON_VERSIONS
17
+ ]
18
+
19
+
20
+ def run_python_subprocess(args: list[str], env=None) -> CompletedProcess:
21
+ """Invoke python with given args, using an environment identical to current environment."""
22
+ # If running a pex file directly, we invoke the executable pex file again.
23
+ # Otherwise we assume we're running in a pex generated venv and use the python executable.
24
+ cmd = os.getenv("PEX", sys.executable)
25
+
26
+ args = [cmd] + args
27
+ logging.debug(f"Running {args} in {os.path.abspath(os.curdir)}")
28
+ proc = subprocess.run(args, capture_output=True, env=env, check=False)
29
+ return proc
30
+
31
+
32
+ def run_self_module(module_name, args: list[str], env=None) -> CompletedProcess:
33
+ """Invoke this executable again with -m {module}."""
34
+ return run_python_subprocess(["-m", module_name, *args], env=env)
35
+
36
+
37
+ def get_pex_flags(python_version: version.Version, build_sdists: bool = True) -> list[str]:
38
+ """python_version should includes the major and minor version only, eg Version('3.11')."""
39
+ if python_version not in TARGET_PYTHON_VERSIONS:
40
+ raise ValueError(
41
+ f"Unsupported python version {python_version}. Supported: {TARGET_PYTHON_VERSIONS}."
42
+ )
43
+ version_tag = f"{python_version.major}{python_version.minor}" # eg '38'
44
+ # Resolves dependencies using the local interpreter, effectively allowing source distributions
45
+ # to work (since they get built by the local interpreter).
46
+ # If build_sdists is false and a binary distribution matching the platform below is not
47
+ # available for a dependency, then the build will fail.
48
+ # see also https://linear.app/elementl/issue/CLOUD-2023/pex-builds-fail-for-dbt-core-dependency
49
+ resolve_local = ["--resolve-local-platforms"] if build_sdists else []
50
+ # This is mainly useful in local mac test environments
51
+ include_current = ["--platform=current"] if os.getenv("PEX_INCLUDE_CURRENT_PLATFORM") else []
52
+
53
+ complete_platform = os.getenv(
54
+ "PEX_COMPLETE_PLATFORM", json.dumps(COMPLETE_PLATFORMS[version_tag])
55
+ )
56
+
57
+ return [
58
+ # this complete platform matches what can run on our serverless base images
59
+ f"--complete-platform={complete_platform}",
60
+ *include_current,
61
+ # this ensures PEX_PATH is not cleared and any subprocess invoked can also use this.
62
+ # this is important for running console scripts that use the pex environment (eg dbt)
63
+ "--no-strip-pex-env",
64
+ # use the latest version of pip bundled with pex - this will typically provide
65
+ # the best dependency resolution logic. added in
66
+ # https://github.com/pantsbuild/pex/releases/tag/v2.1.132
67
+ "--pip-version=latest",
68
+ "-v", # verbose logging, level 3
69
+ "-v",
70
+ "-v",
71
+ *resolve_local,
72
+ ]
73
+
74
+
75
+ def build_pex(
76
+ sources_directories: list[str],
77
+ requirements_filepaths: list[str],
78
+ pex_flags: list[str],
79
+ output_pex_path: str,
80
+ pex_root: str | None = None,
81
+ ) -> CompletedProcess:
82
+ """Invoke pex with common build flags and pass parameters through to specific pex flags.
83
+
84
+ sources_directories: passed to the pex -D flag (aka --source-directories)
85
+ requirements_filepaths: passed to the pex -r flag (aka --requirement)
86
+
87
+ Platform:
88
+ For the pex --platform tag used below, see pex --help and
89
+ https://peps.python.org/pep-0425/
90
+ https://peps.python.org/pep-0427/
91
+
92
+ Packages for the current platform are only included if requested with PEX_INCLUDE_CURRENT_PLATFORM
93
+ The manylinux platform ensures pexes built on local machines (macos, windows) are compatible
94
+ with linux on cloud.
95
+
96
+ The python_version tuple eg ('3', '9') determines the target runtime environment. In theory
97
+ we can build a pex in an environment without the target python version present.
98
+ """
99
+ flags = pex_flags.copy()
100
+ if not sources_directories and not requirements_filepaths:
101
+ raise ValueError("At least one of sources_directories or requirements_filepath required.")
102
+ for src_dir in sources_directories:
103
+ flags.extend(["-D", src_dir])
104
+ for req_file in requirements_filepaths:
105
+ flags.extend(["-r", req_file])
106
+ pex_args = [*flags, "-o", output_pex_path]
107
+ if pex_root:
108
+ pex_args.extend(["--pex-root", pex_root])
109
+ return run_pex_command(pex_args)
110
+
111
+
112
+ def run_pex_command(args: list[str]) -> CompletedProcess:
113
+ # https://github.com/pantsbuild/pex/issues/1969#issuecomment-1336105021
114
+ env = {**os.environ, "_PEX_FILE_LOCK_STYLE": "bsd"}
115
+
116
+ return run_self_module("pex", args, env=env)
117
+
118
+
119
+ def run_dagster_cloud_cli_command(args: list[str]) -> CompletedProcess:
120
+ return run_self_module("dagster_cloud_cli.entrypoint", args)
121
+
122
+
123
+ def run_dagster_command(args: list[str]) -> CompletedProcess:
124
+ return run_self_module("dagster", args)
125
+
126
+
127
+ def get_pex_info(pex_filepath):
128
+ with ZipFile(pex_filepath) as pex_zip:
129
+ return json.load(pex_zip.open("PEX-INFO"))
130
+
131
+
132
+ def build_pex_tag(filepaths: list[str]) -> str:
133
+ return "files=" + ":".join(sorted(os.path.basename(filepath) for filepath in filepaths))
134
+
135
+
136
+ def python_interpreter_for(python_version: version.Version) -> str:
137
+ return "python" + str(python_version) # eg 'python3.11'
138
+
139
+
140
+ def python_version_option():
141
+ """Reusable click.option."""
142
+ return click.option(
143
+ "--python-version",
144
+ type=click.Choice(SUPPORTED_PYTHON_VERSIONS),
145
+ default=DEFAULT_PYTHON_VERSION,
146
+ show_default=True,
147
+ help="Target Python version.",
148
+ )
149
+
150
+
151
+ def parse_python_version(python_version: str) -> version.Version:
152
+ return version.Version(python_version)
153
+
154
+
155
+ def parse_kv(ctx, param: str, value: str | None):
156
+ if not value:
157
+ return {}
158
+ try:
159
+ return dict(part.split("=", 1) for part in value.split(","))
160
+ except ValueError as err:
161
+ raise ValueError(f"Value {value!r} could not be parsed: {err}")
162
+
163
+
164
+ def indent(text: str, prefix="| "):
165
+ return "".join([prefix + line for line in text.splitlines(keepends=True)])
@@ -0,0 +1,145 @@
1
+ from collections.abc import Mapping
2
+ from typing import Any
3
+
4
+ from dagster_shared import check
5
+ from dagster_shared.record import IHaveNew, LegacyNamedTupleMixin, copy, record, record_custom
6
+ from dagster_shared.serdes import whitelist_for_serdes
7
+ from dagster_shared.serdes.objects.models.defs_state_info import DefsStateInfo
8
+ from dagster_shared.serdes.serdes import serialize_value
9
+
10
+ from dagster_cloud_cli.core.agent_queue import AgentQueue
11
+
12
+
13
+ @whitelist_for_serdes
14
+ @record(kw_only=False)
15
+ class GitMetadata:
16
+ commit_hash: str | None = None
17
+ url: str | None = None
18
+
19
+
20
+ @whitelist_for_serdes
21
+ @record(kw_only=False)
22
+ class PexMetadata:
23
+ # pex_tag is a string like 'deps-234y4384.pex:source-39y3474.pex' that idenfies
24
+ # the pex files to execute
25
+ pex_tag: str
26
+ # python_version determines which pex base docker image to use
27
+ # only one of PexMetadata.python_version or CodeLocationDeployData.image should be specified
28
+ python_version: str | None = None
29
+
30
+
31
+ # History of CodeLocationDeployData
32
+ # 1. Removal of `enable_metrics` field
33
+ # 2. Renamed from `CodeDeploymentMetadata` to `CodeLocationDeployData``
34
+ @whitelist_for_serdes(storage_name="CodeDeploymentMetadata")
35
+ @record_custom
36
+ class CodeLocationDeployData(IHaveNew, LegacyNamedTupleMixin):
37
+ image: str | None
38
+ python_file: str | None
39
+ package_name: str | None
40
+ module_name: str | None
41
+ working_directory: str | None
42
+ executable_path: str | None
43
+ attribute: str | None
44
+ git_metadata: GitMetadata | None
45
+ container_context: Mapping[str, Any]
46
+ cloud_context_env: Mapping[str, Any]
47
+ pex_metadata: PexMetadata | None
48
+ agent_queue: AgentQueue | None
49
+ autoload_defs_module_name: str | None
50
+ defs_state_info: DefsStateInfo | None
51
+
52
+ def __new__(
53
+ cls,
54
+ image: str | None = None,
55
+ python_file: str | None = None,
56
+ package_name: str | None = None,
57
+ module_name: str | None = None,
58
+ working_directory: str | None = None,
59
+ executable_path: str | None = None,
60
+ attribute: str | None = None,
61
+ git_metadata: GitMetadata | None = None,
62
+ container_context: Mapping[str, Any] | None = None,
63
+ cloud_context_env: Mapping[str, Any] | None = None,
64
+ pex_metadata: PexMetadata | None = None,
65
+ agent_queue: AgentQueue | None = None,
66
+ autoload_defs_module_name: str | None = None,
67
+ defs_state_info: DefsStateInfo | None = None,
68
+ ):
69
+ check.invariant(
70
+ len(
71
+ [
72
+ val
73
+ for val in [
74
+ python_file,
75
+ package_name,
76
+ module_name,
77
+ autoload_defs_module_name,
78
+ ]
79
+ if val
80
+ ]
81
+ )
82
+ == 1,
83
+ "Must supply exactly one of python_file, package_name, module_name, or autoload_defs_module_name.",
84
+ )
85
+
86
+ return super().__new__(
87
+ cls,
88
+ image=image,
89
+ python_file=python_file,
90
+ package_name=package_name,
91
+ module_name=module_name,
92
+ working_directory=working_directory,
93
+ executable_path=executable_path,
94
+ attribute=attribute,
95
+ git_metadata=git_metadata,
96
+ container_context=container_context or {},
97
+ cloud_context_env=cloud_context_env or {},
98
+ pex_metadata=pex_metadata,
99
+ agent_queue=agent_queue,
100
+ autoload_defs_module_name=autoload_defs_module_name,
101
+ defs_state_info=defs_state_info,
102
+ )
103
+
104
+ def with_cloud_context_env(
105
+ self,
106
+ cloud_context_env: Mapping[str, Any],
107
+ ) -> "CodeLocationDeployData":
108
+ return copy(self, cloud_context_env=cloud_context_env)
109
+
110
+ def get_multipex_server_command(
111
+ self,
112
+ port: int | None,
113
+ socket: str | None = None,
114
+ metrics_enabled: bool = False,
115
+ ) -> list[str]:
116
+ return (
117
+ ["dagster-cloud", "pex", "grpc", "--host", "0.0.0.0"]
118
+ + (["--port", str(port)] if port else [])
119
+ + (["--socket", str(socket)] if socket else [])
120
+ + (["--enable-metrics"] if metrics_enabled else [])
121
+ + (
122
+ ["--defs-state-info", serialize_value(self.defs_state_info)]
123
+ if self.defs_state_info
124
+ else []
125
+ )
126
+ )
127
+
128
+ def get_multipex_server_env(self) -> dict[str, str]:
129
+ return {"DAGSTER_CURRENT_IMAGE": self.image} if self.image else {}
130
+
131
+ def get_grpc_server_command(self, metrics_enabled: bool = False) -> list[str]:
132
+ return (
133
+ ([self.executable_path, "-m"] if self.executable_path else [])
134
+ + [
135
+ "dagster",
136
+ "api",
137
+ "grpc",
138
+ ]
139
+ + (["--enable-metrics"] if metrics_enabled else [])
140
+ + (
141
+ ["--defs-state-info", serialize_value(self.defs_state_info)]
142
+ if self.defs_state_info
143
+ else []
144
+ )
145
+ )
@@ -0,0 +1,98 @@
1
+ import base64
2
+ import importlib.resources
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import uuid
7
+ from collections.abc import Generator
8
+ from contextlib import ExitStack, contextmanager
9
+
10
+ from dagster_cloud_cli import ui
11
+ from dagster_cloud_cli.utils import DEFAULT_PYTHON_VERSION
12
+
13
+
14
+ def verify_docker() -> None:
15
+ if subprocess.call("docker -v", shell=True) != 0:
16
+ raise ui.error("Docker must be installed locally to deploy to Dagster Cloud Serverless")
17
+
18
+
19
+ @contextmanager
20
+ def _template_dockerfile(env_vars, custom_base_image=None) -> Generator[bytes, None, None]:
21
+ DOCKERFILE_TEMPLATE = str(
22
+ importlib.resources.files("dagster_cloud_cli") / "commands/serverless/Dockerfile"
23
+ )
24
+ base_image_command = (
25
+ f"FROM {custom_base_image}"
26
+ if custom_base_image
27
+ else f"FROM public.ecr.aws/docker/library/python:{DEFAULT_PYTHON_VERSION}-slim"
28
+ )
29
+ with open(DOCKERFILE_TEMPLATE, encoding="utf-8") as template:
30
+ dockerfile_content = "\n".join(
31
+ [base_image_command, template.read(), *[f"ENV {env_var}" for env_var in env_vars]]
32
+ )
33
+
34
+ yield bytes(dockerfile_content, "utf-8")
35
+
36
+
37
+ def build_image(
38
+ source_directory,
39
+ image: str,
40
+ registry_info,
41
+ env_vars: list[str],
42
+ base_image: str | None,
43
+ use_editable_dagster: bool,
44
+ dockerfile_path: str | None = None,
45
+ ) -> int:
46
+ registry = registry_info["registry_url"]
47
+
48
+ with ExitStack() as stack:
49
+ if dockerfile_path:
50
+ file_path = dockerfile_path
51
+ build_input = None
52
+ else:
53
+ file_path = "-"
54
+ build_input = stack.enter_context(_template_dockerfile(env_vars, base_image))
55
+
56
+ cmd = [
57
+ "docker",
58
+ "build",
59
+ source_directory,
60
+ "-t",
61
+ f"{registry}:{image}",
62
+ "-f",
63
+ file_path,
64
+ "--platform",
65
+ "linux/amd64",
66
+ ]
67
+ if use_editable_dagster:
68
+ cmd += [
69
+ "--build-context",
70
+ f"oss={os.environ['DAGSTER_GIT_REPO_DIR']}",
71
+ "--build-context",
72
+ f"internal={os.environ['DAGSTER_INTERNAL_GIT_REPO_DIR']}",
73
+ ]
74
+ return subprocess.run(cmd, input=build_input, check=True).returncode
75
+
76
+
77
+ def upload_image(image, registry_info) -> int:
78
+ registry = registry_info["registry_url"]
79
+ aws_token = registry_info["aws_auth_token"]
80
+ if not registry or not aws_token:
81
+ raise ui.error(
82
+ "No registry found. You may need to wait for your Dagster serverless deployment to"
83
+ " activate."
84
+ )
85
+
86
+ username, password = base64.b64decode(aws_token).decode("utf-8").split(":")
87
+ subprocess.check_output(
88
+ f"echo {password} | docker login --username {username} --password-stdin {registry}",
89
+ shell=True,
90
+ )
91
+ return subprocess.call(
92
+ ["docker", "push", f"{registry}:{image}"], stderr=sys.stderr, stdout=sys.stdout
93
+ )
94
+
95
+
96
+ def default_image_tag(deployment: str, location_name: str, commit_hash: str | None) -> str:
97
+ commit_hash = commit_hash if commit_hash else str(uuid.uuid4().hex)
98
+ return f"{deployment}-{location_name}-{commit_hash}"