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.
- dagster_cloud_cli/__init__.py +5 -0
- dagster_cloud_cli/commands/__init__.py +0 -0
- dagster_cloud_cli/commands/branch_deployment/__init__.py +154 -0
- dagster_cloud_cli/commands/ci/__init__.py +1207 -0
- dagster_cloud_cli/commands/ci/checks.py +135 -0
- dagster_cloud_cli/commands/ci/report.py +49 -0
- dagster_cloud_cli/commands/ci/state.py +129 -0
- dagster_cloud_cli/commands/ci/utils.py +22 -0
- dagster_cloud_cli/commands/config.py +250 -0
- dagster_cloud_cli/commands/deployment/__init__.py +108 -0
- dagster_cloud_cli/commands/deployment/alert_policies/__init__.py +0 -0
- dagster_cloud_cli/commands/deployment/alert_policies/commands.py +52 -0
- dagster_cloud_cli/commands/integration/__init__.py +6 -0
- dagster_cloud_cli/commands/integration/atlan/__init__.py +94 -0
- dagster_cloud_cli/commands/job/__init__.py +117 -0
- dagster_cloud_cli/commands/metrics.py +114 -0
- dagster_cloud_cli/commands/organization/__init__.py +134 -0
- dagster_cloud_cli/commands/organization/saml/__init__.py +0 -0
- dagster_cloud_cli/commands/organization/saml/commands.py +85 -0
- dagster_cloud_cli/commands/run/__init__.py +27 -0
- dagster_cloud_cli/commands/serverless/Dockerfile +38 -0
- dagster_cloud_cli/commands/serverless/Dockerfile.dagster-cloud-cli +20 -0
- dagster_cloud_cli/commands/serverless/__init__.py +560 -0
- dagster_cloud_cli/commands/workspace/__init__.py +358 -0
- dagster_cloud_cli/config/__init__.py +6 -0
- dagster_cloud_cli/config/jinja_template_loader.py +45 -0
- dagster_cloud_cli/config/list_merge_strategies.py +77 -0
- dagster_cloud_cli/config/loader.py +23 -0
- dagster_cloud_cli/config/merger.py +92 -0
- dagster_cloud_cli/config/models.py +76 -0
- dagster_cloud_cli/config_utils.py +506 -0
- dagster_cloud_cli/core/__init__.py +0 -0
- dagster_cloud_cli/core/agent_queue.py +3 -0
- dagster_cloud_cli/core/alert_types.py +26 -0
- dagster_cloud_cli/core/artifacts.py +192 -0
- dagster_cloud_cli/core/docker_runner.py +57 -0
- dagster_cloud_cli/core/errors.py +32 -0
- dagster_cloud_cli/core/graphql_client.py +420 -0
- dagster_cloud_cli/core/headers/__init__.py +0 -0
- dagster_cloud_cli/core/headers/auth.py +16 -0
- dagster_cloud_cli/core/headers/impl.py +31 -0
- dagster_cloud_cli/core/headers/versioning/__init__.py +0 -0
- dagster_cloud_cli/core/headers/versioning/constants.py +2 -0
- dagster_cloud_cli/core/pex_builder/__init__.py +8 -0
- dagster_cloud_cli/core/pex_builder/code_location.py +126 -0
- dagster_cloud_cli/core/pex_builder/deploy.py +255 -0
- dagster_cloud_cli/core/pex_builder/deps.py +495 -0
- dagster_cloud_cli/core/pex_builder/git_context.py +29 -0
- dagster_cloud_cli/core/pex_builder/github_context.py +178 -0
- dagster_cloud_cli/core/pex_builder/gitlab_context.py +30 -0
- dagster_cloud_cli/core/pex_builder/parse_workspace.py +39 -0
- dagster_cloud_cli/core/pex_builder/pex_registry.py +148 -0
- dagster_cloud_cli/core/pex_builder/platforms.py +6105 -0
- dagster_cloud_cli/core/pex_builder/selftest.py +14 -0
- dagster_cloud_cli/core/pex_builder/source.py +166 -0
- dagster_cloud_cli/core/pex_builder/util.py +165 -0
- dagster_cloud_cli/core/workspace.py +145 -0
- dagster_cloud_cli/docker_utils.py +98 -0
- dagster_cloud_cli/entrypoint.py +209 -0
- dagster_cloud_cli/gql.py +1025 -0
- dagster_cloud_cli/pex_utils.py +180 -0
- dagster_cloud_cli/py.typed +0 -0
- dagster_cloud_cli/types.py +78 -0
- dagster_cloud_cli/ui.py +101 -0
- dagster_cloud_cli/utils.py +107 -0
- dagster_cloud_cli/version.py +1 -0
- dagster_cloud_cli-1.12.16.dist-info/METADATA +31 -0
- dagster_cloud_cli-1.12.16.dist-info/RECORD +71 -0
- dagster_cloud_cli-1.12.16.dist-info/WHEEL +5 -0
- dagster_cloud_cli-1.12.16.dist-info/entry_points.txt +3 -0
- 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}"
|