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,52 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
from typer import Option, Typer
|
|
5
|
+
|
|
6
|
+
from dagster_cloud_cli import gql, ui
|
|
7
|
+
from dagster_cloud_cli.config_utils import dagster_cloud_options
|
|
8
|
+
|
|
9
|
+
DEFAULT_ALERT_POLICIES_YAML_FILENAME = "alert_policies.yaml"
|
|
10
|
+
|
|
11
|
+
app = Typer(help="Interact with your alert policies.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command(name="list")
|
|
15
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
16
|
+
def list_command(
|
|
17
|
+
api_token: str,
|
|
18
|
+
url: str,
|
|
19
|
+
deployment: str | None,
|
|
20
|
+
):
|
|
21
|
+
"""List your alert policies, output in YAML format."""
|
|
22
|
+
with gql.graphql_client_from_url(url, api_token, deployment_name=deployment) as client:
|
|
23
|
+
alert_policies_response = gql.get_alert_policies(client)
|
|
24
|
+
|
|
25
|
+
ui.print_yaml(alert_policies_response)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command(name="sync")
|
|
29
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
30
|
+
def sync_command(
|
|
31
|
+
api_token: str,
|
|
32
|
+
url: str,
|
|
33
|
+
deployment: str | None,
|
|
34
|
+
alert_policies_file: Path = Option(
|
|
35
|
+
DEFAULT_ALERT_POLICIES_YAML_FILENAME,
|
|
36
|
+
"--alert-policies",
|
|
37
|
+
"-a",
|
|
38
|
+
exists=True,
|
|
39
|
+
help="Path to alert policies file.",
|
|
40
|
+
),
|
|
41
|
+
):
|
|
42
|
+
"""Sync your YAML configured alert policies to Dagster Cloud."""
|
|
43
|
+
with gql.graphql_client_from_url(url, api_token, deployment_name=deployment) as client:
|
|
44
|
+
with open(str(alert_policies_file), encoding="utf8") as f:
|
|
45
|
+
config = yaml.load(f.read(), Loader=yaml.SafeLoader)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
alert_policies = gql.reconcile_alert_policies(client, config)
|
|
49
|
+
|
|
50
|
+
ui.print(f"Synced alert policies: {', '.join(alert_policies)}")
|
|
51
|
+
except Exception as e:
|
|
52
|
+
raise ui.error(str(e))
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import validators
|
|
5
|
+
from typer import Argument, Typer
|
|
6
|
+
|
|
7
|
+
from dagster_cloud_cli import gql, ui
|
|
8
|
+
from dagster_cloud_cli.config_utils import dagster_cloud_options
|
|
9
|
+
|
|
10
|
+
app = Typer(help="Customize your Atlan integration.")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command(name="set-settings")
|
|
14
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
15
|
+
def set_atlan_settings_command(
|
|
16
|
+
atlan_token: Annotated[
|
|
17
|
+
str,
|
|
18
|
+
Argument(
|
|
19
|
+
help="The token to use to access the Atlan API.",
|
|
20
|
+
),
|
|
21
|
+
],
|
|
22
|
+
atlan_domain: Annotated[
|
|
23
|
+
str,
|
|
24
|
+
Argument(
|
|
25
|
+
help="The domain of your Atlan tenant. Eg. your-organization.atlan.com.",
|
|
26
|
+
),
|
|
27
|
+
],
|
|
28
|
+
api_token: str,
|
|
29
|
+
url: str,
|
|
30
|
+
):
|
|
31
|
+
"""Upload your Atlan settings to enable the Dagster<>Atlan integration in Dagster Cloud."""
|
|
32
|
+
if not validators.domain(atlan_domain):
|
|
33
|
+
ui.print(
|
|
34
|
+
"Invalid domain. "
|
|
35
|
+
"Please provide the domain of your Altan tenant using the following format: your-organization.atlan.com."
|
|
36
|
+
)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
with gql.graphql_client_from_url(url, api_token) as client:
|
|
41
|
+
organization, _ = gql.set_atlan_integration_settings(
|
|
42
|
+
client, token=atlan_token, domain=atlan_domain
|
|
43
|
+
)
|
|
44
|
+
ui.print(f"Atlan settings successfully set for organization: {organization}")
|
|
45
|
+
|
|
46
|
+
except Exception as e:
|
|
47
|
+
ui.print(f"Failed to set Atlan settings. Exception: {e}")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command(name="delete-settings")
|
|
52
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
53
|
+
def delete_atlan_settings_command(
|
|
54
|
+
api_token: str,
|
|
55
|
+
url: str,
|
|
56
|
+
):
|
|
57
|
+
"""Delete your Atlan settings to disable the Dagster<>Atlan integration in Dagster Cloud."""
|
|
58
|
+
try:
|
|
59
|
+
with gql.graphql_client_from_url(url, api_token) as client:
|
|
60
|
+
organization, _ = gql.delete_atlan_integration_settings(client)
|
|
61
|
+
ui.print(f"Atlan settings successfully deleted for organization: {organization}")
|
|
62
|
+
except Exception as e:
|
|
63
|
+
ui.print(f"Failed to delete Atlan settings. Exception: {e}")
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command(name="get-settings")
|
|
68
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
69
|
+
def get_atlan_settings_command(
|
|
70
|
+
api_token: str,
|
|
71
|
+
url: str,
|
|
72
|
+
):
|
|
73
|
+
"""Get your current Atlan settings from Dagster Cloud."""
|
|
74
|
+
with gql.graphql_client_from_url(url, api_token) as client:
|
|
75
|
+
settings = gql.get_atlan_integration_settings(client)
|
|
76
|
+
ui.print(f"Token: {settings['token']}")
|
|
77
|
+
ui.print(f"Domain: {settings['domain']}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.command(name="preflight-check")
|
|
81
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
82
|
+
def preflight_check_command(
|
|
83
|
+
api_token: str,
|
|
84
|
+
url: str,
|
|
85
|
+
):
|
|
86
|
+
"""Run a preflight check on your Atlan integration settings."""
|
|
87
|
+
with gql.graphql_client_from_url(url, api_token) as client:
|
|
88
|
+
result = gql.atlan_integration_preflight_check(client)
|
|
89
|
+
if result["success"]:
|
|
90
|
+
ui.print("Preflight check passed successfully!")
|
|
91
|
+
else:
|
|
92
|
+
ui.print("Preflight check failed:")
|
|
93
|
+
ui.print(f" Error code: {result['error_code']}")
|
|
94
|
+
ui.print(f" Error message: {result['error_message']}")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from typer import Typer
|
|
9
|
+
|
|
10
|
+
from dagster_cloud_cli import gql, ui
|
|
11
|
+
from dagster_cloud_cli.config_utils import dagster_cloud_options
|
|
12
|
+
|
|
13
|
+
app = Typer(help="Commands for working with Dagster Cloud jobs.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SINGLETON_REPOSITORY_NAME = "__repository__"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command(name="launch")
|
|
20
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
21
|
+
def launch(
|
|
22
|
+
api_token: str,
|
|
23
|
+
url: str,
|
|
24
|
+
deployment: str | None,
|
|
25
|
+
location: str = typer.Option(..., "-l", "--location", help="Location name in the deployment."),
|
|
26
|
+
job: str = typer.Option(..., "-j", "--job", help="Job name to run."),
|
|
27
|
+
repository: str = typer.Option(
|
|
28
|
+
None,
|
|
29
|
+
"-r",
|
|
30
|
+
"--repository",
|
|
31
|
+
help=(
|
|
32
|
+
"Repository in the specified code location. Required if a repository is defined in the"
|
|
33
|
+
" specified code location."
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
tags: str = typer.Option(None, "--tags", help="JSON dict of tags to use for this job run."),
|
|
37
|
+
config: str = typer.Option(
|
|
38
|
+
None, "--config-json", help="JSON string of run config to use for this job run"
|
|
39
|
+
),
|
|
40
|
+
asset_keys: list[str] | None = typer.Option(
|
|
41
|
+
None,
|
|
42
|
+
"--asset-key",
|
|
43
|
+
help=(
|
|
44
|
+
"Asset key to materialize. Can be specified multiple times to materialize multiple"
|
|
45
|
+
" assets."
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
wait: bool = typer.Option(
|
|
49
|
+
False,
|
|
50
|
+
"-w",
|
|
51
|
+
"--wait",
|
|
52
|
+
help=(
|
|
53
|
+
"Wait for the run to finish and print the result. This will block until the run is"
|
|
54
|
+
" finished."
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
interval: int = typer.Option(
|
|
58
|
+
30,
|
|
59
|
+
"-i",
|
|
60
|
+
"--interval",
|
|
61
|
+
help=(
|
|
62
|
+
"Interval in seconds to wait between checking the status of the run. Only used if"
|
|
63
|
+
" --wait is specified."
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
):
|
|
67
|
+
"""Launch a run for a job."""
|
|
68
|
+
loaded_tags: dict[str, Any] = json.loads(tags) if tags else {}
|
|
69
|
+
loaded_config: dict[str, Any] = json.loads(config) if config else {}
|
|
70
|
+
|
|
71
|
+
repository = repository or SINGLETON_REPOSITORY_NAME
|
|
72
|
+
|
|
73
|
+
headers: dict[str, str] = {
|
|
74
|
+
"Idempotency-Key": str(uuid4()),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
with gql.graphql_client_from_url(
|
|
78
|
+
url,
|
|
79
|
+
api_token,
|
|
80
|
+
deployment_name=deployment,
|
|
81
|
+
headers=headers,
|
|
82
|
+
retries=int(os.getenv("DAGSTER_CLOUD_JOB_LAUNCH_RETRIES", "5")),
|
|
83
|
+
) as client:
|
|
84
|
+
run_id = gql.launch_run(
|
|
85
|
+
client,
|
|
86
|
+
location,
|
|
87
|
+
repository,
|
|
88
|
+
job,
|
|
89
|
+
loaded_tags,
|
|
90
|
+
loaded_config,
|
|
91
|
+
asset_keys=asset_keys,
|
|
92
|
+
)
|
|
93
|
+
if wait:
|
|
94
|
+
ui.print(f"Run {run_id} launched, waiting for completion...")
|
|
95
|
+
status = None
|
|
96
|
+
with gql.graphql_client_from_url(url, api_token, deployment_name=deployment) as client:
|
|
97
|
+
while True:
|
|
98
|
+
time.sleep(interval)
|
|
99
|
+
try:
|
|
100
|
+
status = gql.run_status(client, run_id)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
ui.error(f"Failed to get status for run {run_id}: {e}.")
|
|
103
|
+
|
|
104
|
+
if status in ["SUCCESS", "FAILURE", "CANCELED"]:
|
|
105
|
+
break
|
|
106
|
+
else:
|
|
107
|
+
ui.print(f"Run {run_id} is in progress (status: {status})...")
|
|
108
|
+
|
|
109
|
+
if status == "SUCCESS":
|
|
110
|
+
ui.print(f"Run {run_id} finished successfully.")
|
|
111
|
+
else:
|
|
112
|
+
ui.error(
|
|
113
|
+
f"Run {run_id} failed with status '{status}'. Check the Dagster Cloud UI for more details."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
else:
|
|
117
|
+
ui.print(run_id)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from contextvars import ContextVar
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from functools import wraps
|
|
6
|
+
|
|
7
|
+
from dagster_cloud_cli import gql
|
|
8
|
+
from dagster_cloud_cli.types import CliEventTags, CliEventType
|
|
9
|
+
from dagster_cloud_cli.ui import ExitWithMessage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_source() -> CliEventTags.source:
|
|
13
|
+
default = CliEventTags.source.cli
|
|
14
|
+
unknown = CliEventTags.source.unknown
|
|
15
|
+
env_map = {
|
|
16
|
+
"BITBUCKET_BUILD_NUMBER": (
|
|
17
|
+
CliEventTags.source.bitbucket # https://support.atlassian.com/bitbucket-cloud/docs/variables-and-secrets/
|
|
18
|
+
),
|
|
19
|
+
"BUILDKITE": (
|
|
20
|
+
CliEventTags.source.buildkite # https://buildkite.com/docs/pipelines/environment-variables
|
|
21
|
+
),
|
|
22
|
+
"CIRCLECI": (
|
|
23
|
+
CliEventTags.source.circle_ci # https://circleci.com/docs/variables/#built-in-environment-variables
|
|
24
|
+
),
|
|
25
|
+
"CODEBUILD_BUILD_ID": (
|
|
26
|
+
CliEventTags.source.codebuild # https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
|
|
27
|
+
),
|
|
28
|
+
"GITHUB_ACTION": (
|
|
29
|
+
CliEventTags.source.github # https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
|
|
30
|
+
),
|
|
31
|
+
"GITLAB_CI": (
|
|
32
|
+
CliEventTags.source.gitlab # https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
|
|
33
|
+
),
|
|
34
|
+
"JENKINS_URL": (
|
|
35
|
+
CliEventTags.source.jenkins # https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
|
36
|
+
),
|
|
37
|
+
"TRAVIS": (
|
|
38
|
+
CliEventTags.source.travis # https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
|
|
39
|
+
),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
sources = [value for key, value in env_map.items() if os.getenv(key)]
|
|
43
|
+
if len(sources) > 1:
|
|
44
|
+
return unknown
|
|
45
|
+
elif sources:
|
|
46
|
+
return sources[0]
|
|
47
|
+
else:
|
|
48
|
+
return default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
mark_cli_event_context_tags: ContextVar[list[str]] = ContextVar("mark_cli_event_context_tags")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def instrument_add_tags(tags: list[Enum]):
|
|
55
|
+
"""Add tags to the instrumentation within the current context.
|
|
56
|
+
If this is not wrapped in an @instrument decorator, this call has no effect.
|
|
57
|
+
"""
|
|
58
|
+
new_tags = [str(tag.value) for tag in tags or []]
|
|
59
|
+
context_tags = mark_cli_event_context_tags.get([])
|
|
60
|
+
mark_cli_event_context_tags.set(context_tags + new_tags)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def instrument(event_type: CliEventType, tags: list[Enum] | None = None):
|
|
64
|
+
str_tags = [str(tag.value) for tag in tags or []]
|
|
65
|
+
str_tags.append(str(get_source().value))
|
|
66
|
+
|
|
67
|
+
def decorator(func):
|
|
68
|
+
@wraps(func)
|
|
69
|
+
def wrapper(*args, **kwargs):
|
|
70
|
+
url = kwargs.get("url")
|
|
71
|
+
api_token = kwargs.get("api_token")
|
|
72
|
+
start_time = time.time()
|
|
73
|
+
|
|
74
|
+
with gql.graphql_client_from_url(
|
|
75
|
+
url=str(url), token=str(api_token), retries=0
|
|
76
|
+
) as client:
|
|
77
|
+
context_token = mark_cli_event_context_tags.set([])
|
|
78
|
+
try:
|
|
79
|
+
result = func(*args, **kwargs)
|
|
80
|
+
gql.mark_cli_event(
|
|
81
|
+
client=client,
|
|
82
|
+
event_type=event_type,
|
|
83
|
+
duration_seconds=time.time() - start_time,
|
|
84
|
+
success=True,
|
|
85
|
+
tags=str_tags + mark_cli_event_context_tags.get([]),
|
|
86
|
+
)
|
|
87
|
+
return result
|
|
88
|
+
except ExitWithMessage as ex:
|
|
89
|
+
gql.mark_cli_event(
|
|
90
|
+
client=client,
|
|
91
|
+
event_type=event_type,
|
|
92
|
+
duration_seconds=time.time() - start_time,
|
|
93
|
+
success=False,
|
|
94
|
+
tags=str_tags + mark_cli_event_context_tags.get([]),
|
|
95
|
+
message=ex.message,
|
|
96
|
+
)
|
|
97
|
+
raise
|
|
98
|
+
|
|
99
|
+
except Exception:
|
|
100
|
+
gql.mark_cli_event(
|
|
101
|
+
client=client,
|
|
102
|
+
event_type=event_type,
|
|
103
|
+
duration_seconds=time.time() - start_time,
|
|
104
|
+
success=False,
|
|
105
|
+
tags=str_tags + mark_cli_event_context_tags.get([]),
|
|
106
|
+
message="unexpected error",
|
|
107
|
+
)
|
|
108
|
+
raise
|
|
109
|
+
finally:
|
|
110
|
+
mark_cli_event_context_tags.reset(context_token)
|
|
111
|
+
|
|
112
|
+
return wrapper
|
|
113
|
+
|
|
114
|
+
return decorator
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from typer import Argument, Typer
|
|
6
|
+
|
|
7
|
+
from dagster_cloud_cli import gql, ui
|
|
8
|
+
from dagster_cloud_cli.commands.organization.saml import commands as saml_cli
|
|
9
|
+
from dagster_cloud_cli.config_utils import dagster_cloud_options
|
|
10
|
+
from dagster_cloud_cli.core.artifacts import download_artifact, upload_artifact
|
|
11
|
+
from dagster_cloud_cli.core.headers.auth import DagsterCloudInstanceScope
|
|
12
|
+
|
|
13
|
+
app = Typer(help="Customize your Dagster Cloud organization.")
|
|
14
|
+
settings_app = Typer(help="Customize your organization settings.")
|
|
15
|
+
|
|
16
|
+
app.add_typer(settings_app, name="settings", no_args_is_help=True)
|
|
17
|
+
settings_app.add_typer(saml_cli.app, name="saml", no_args_is_help=True)
|
|
18
|
+
|
|
19
|
+
# Legacy, to support the old `dagster-cloud settings` path
|
|
20
|
+
# New command is `dagster-cloud organization
|
|
21
|
+
legacy_settings_app = Typer(
|
|
22
|
+
help=(
|
|
23
|
+
"[Deprecated, in favor of dagster-cloud organization] Customize your dagster-cloud"
|
|
24
|
+
" settings."
|
|
25
|
+
),
|
|
26
|
+
hidden=True,
|
|
27
|
+
)
|
|
28
|
+
legacy_settings_app.add_typer(saml_cli.app, name="saml")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@settings_app.command(name="set-from-file")
|
|
32
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
33
|
+
def set_from_filecommand(
|
|
34
|
+
organization: str,
|
|
35
|
+
api_token: str,
|
|
36
|
+
url: str,
|
|
37
|
+
file_path: Path = Argument(..., readable=True, metavar="SETTINGS_FILE_PATH"),
|
|
38
|
+
):
|
|
39
|
+
"""Set the Dagster Cloud organization settings from a YAML file."""
|
|
40
|
+
if not url and not organization:
|
|
41
|
+
raise ui.error("Must provide either organization name or URL.")
|
|
42
|
+
if not url:
|
|
43
|
+
url = gql.url_from_config(organization=organization)
|
|
44
|
+
|
|
45
|
+
with open(file_path, encoding="utf8") as f:
|
|
46
|
+
settings = {"settings": yaml.safe_load(f) or {}}
|
|
47
|
+
with gql.graphql_client_from_url(url, api_token) as client:
|
|
48
|
+
gql.set_organization_settings(client, settings)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@settings_app.command(name="get")
|
|
52
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
53
|
+
def get_command(
|
|
54
|
+
organization: str,
|
|
55
|
+
api_token: str,
|
|
56
|
+
url: str,
|
|
57
|
+
):
|
|
58
|
+
"""Get the Dagster Cloud organization settings."""
|
|
59
|
+
if not url and not organization:
|
|
60
|
+
raise ui.error("Must provide either organization name or URL.")
|
|
61
|
+
if not url:
|
|
62
|
+
url = gql.url_from_config(organization=organization)
|
|
63
|
+
|
|
64
|
+
with gql.graphql_client_from_url(url, api_token) as client:
|
|
65
|
+
settings = gql.get_organization_settings(client)
|
|
66
|
+
ui.print_yaml(settings)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command(name="upload-artifact")
|
|
70
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
71
|
+
def upload_artifact_command(
|
|
72
|
+
key: Annotated[
|
|
73
|
+
str,
|
|
74
|
+
Argument(
|
|
75
|
+
help="The key for the artifact being uploaded.",
|
|
76
|
+
),
|
|
77
|
+
],
|
|
78
|
+
path: Annotated[
|
|
79
|
+
str,
|
|
80
|
+
Argument(
|
|
81
|
+
help="The path to the file to upload.",
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
organization: str,
|
|
85
|
+
api_token: str,
|
|
86
|
+
url: str,
|
|
87
|
+
):
|
|
88
|
+
"""Upload a organization scoped artifact."""
|
|
89
|
+
if not url and not organization:
|
|
90
|
+
raise ui.error("Must provide either organization name or URL.")
|
|
91
|
+
if not url:
|
|
92
|
+
url = gql.url_from_config(organization=organization)
|
|
93
|
+
|
|
94
|
+
upload_artifact(
|
|
95
|
+
url=url,
|
|
96
|
+
scope=DagsterCloudInstanceScope.ORGANIZATION,
|
|
97
|
+
api_token=api_token,
|
|
98
|
+
key=key,
|
|
99
|
+
path=path,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command(name="download-artifact")
|
|
104
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
105
|
+
def download_artifact_command(
|
|
106
|
+
key: Annotated[
|
|
107
|
+
str,
|
|
108
|
+
Argument(
|
|
109
|
+
help="The key for the artifact to download.",
|
|
110
|
+
),
|
|
111
|
+
],
|
|
112
|
+
path: Annotated[
|
|
113
|
+
str,
|
|
114
|
+
Argument(
|
|
115
|
+
help="Path to the file that the contents should be written to.",
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
organization: str,
|
|
119
|
+
api_token: str,
|
|
120
|
+
url: str,
|
|
121
|
+
):
|
|
122
|
+
"""Download a organization scoped artifact."""
|
|
123
|
+
if not url and not organization:
|
|
124
|
+
raise ui.error("Must provide either organization name or URL.")
|
|
125
|
+
if not url:
|
|
126
|
+
url = gql.url_from_config(organization=organization)
|
|
127
|
+
|
|
128
|
+
download_artifact(
|
|
129
|
+
url=url,
|
|
130
|
+
scope=DagsterCloudInstanceScope.ORGANIZATION,
|
|
131
|
+
api_token=api_token,
|
|
132
|
+
key=key,
|
|
133
|
+
path=path,
|
|
134
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from typer import Argument, Option, Typer
|
|
5
|
+
|
|
6
|
+
from dagster_cloud_cli import gql, ui
|
|
7
|
+
from dagster_cloud_cli.config_utils import dagster_cloud_options
|
|
8
|
+
from dagster_cloud_cli.core.errors import raise_http_error
|
|
9
|
+
from dagster_cloud_cli.core.headers.auth import DagsterCloudInstanceScope
|
|
10
|
+
from dagster_cloud_cli.core.headers.impl import get_dagster_cloud_api_headers
|
|
11
|
+
|
|
12
|
+
app = Typer(help="Customize your SAML settings.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command(name="upload-identity-provider-metadata")
|
|
16
|
+
@dagster_cloud_options(allow_empty=True)
|
|
17
|
+
def upload_identity_provider_metadata_command(
|
|
18
|
+
api_token: str,
|
|
19
|
+
organization: str,
|
|
20
|
+
url: str = Option(
|
|
21
|
+
None,
|
|
22
|
+
"--url",
|
|
23
|
+
help=(
|
|
24
|
+
"[DEPRECATED] Your Dagster Cloud url, in the form of"
|
|
25
|
+
" 'https://{ORGANIZATION_NAME}.dagster.cloud'."
|
|
26
|
+
),
|
|
27
|
+
hidden=True,
|
|
28
|
+
),
|
|
29
|
+
metadata_file: Path = Argument(..., readable=True, metavar="METADATA_FILE_PATH"),
|
|
30
|
+
):
|
|
31
|
+
"""Upload your identity provider's metadata to enable SAML Single sign-on (SSO)."""
|
|
32
|
+
if not url and not organization:
|
|
33
|
+
raise ui.error("Must provide either organization name or URL.")
|
|
34
|
+
|
|
35
|
+
if not url:
|
|
36
|
+
url = gql.url_from_config(organization=organization)
|
|
37
|
+
|
|
38
|
+
with open(metadata_file, "rb") as f:
|
|
39
|
+
response = requests.post(
|
|
40
|
+
url=f"{url}/upload_idp_metadata",
|
|
41
|
+
headers=get_dagster_cloud_api_headers(api_token, DagsterCloudInstanceScope.DEPLOYMENT),
|
|
42
|
+
files={"metadata.xml": f},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
raise_http_error(response)
|
|
46
|
+
|
|
47
|
+
if not response.text == "SUCCESS":
|
|
48
|
+
raise ui.error(
|
|
49
|
+
"Upload failed; unexpected response. Verify that the correct URL is specified.\n"
|
|
50
|
+
f" {response.text}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
ui.print("The identity provider metadata was successfully uploaded.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command(name="remove-identity-provider-metadata")
|
|
57
|
+
@dagster_cloud_options(allow_empty=True)
|
|
58
|
+
def remove_identity_provider_metadata_command(
|
|
59
|
+
api_token: str,
|
|
60
|
+
organization: str,
|
|
61
|
+
url: str = Option(
|
|
62
|
+
None,
|
|
63
|
+
"--url",
|
|
64
|
+
help=(
|
|
65
|
+
"[DEPRECATED] Your Dagster Cloud url, in the form of"
|
|
66
|
+
" 'https://{ORGANIZATION_NAME}.dagster.cloud'."
|
|
67
|
+
),
|
|
68
|
+
hidden=True,
|
|
69
|
+
),
|
|
70
|
+
):
|
|
71
|
+
"""Remove SAML Single sign-on (SSO) metadata."""
|
|
72
|
+
if not url and not organization:
|
|
73
|
+
raise ui.error("Must provide either organization name or URL.")
|
|
74
|
+
|
|
75
|
+
if not url:
|
|
76
|
+
url = gql.url_from_config(organization=organization)
|
|
77
|
+
|
|
78
|
+
response = requests.post(
|
|
79
|
+
url=f"{url}/remove_idp_metadata",
|
|
80
|
+
headers=get_dagster_cloud_api_headers(api_token, DagsterCloudInstanceScope.DEPLOYMENT),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
raise_http_error(response)
|
|
84
|
+
|
|
85
|
+
ui.print("The identity provider metadata was successfully removed.")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typer import Typer
|
|
3
|
+
|
|
4
|
+
from dagster_cloud_cli import gql, ui
|
|
5
|
+
from dagster_cloud_cli.config_utils import dagster_cloud_options
|
|
6
|
+
|
|
7
|
+
app = Typer(help="Commands for working with Dagster Cloud runs.")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command(name="status")
|
|
11
|
+
@dagster_cloud_options(allow_empty=True, requires_url=True)
|
|
12
|
+
def status(
|
|
13
|
+
api_token: str,
|
|
14
|
+
url: str,
|
|
15
|
+
deployment: str | None,
|
|
16
|
+
run_id: str = typer.Argument(None),
|
|
17
|
+
):
|
|
18
|
+
"""Check the status of a run."""
|
|
19
|
+
# this should be replaced by using the `dagster` CLI against a dagster-cloud `dagster.yaml`
|
|
20
|
+
if not run_id:
|
|
21
|
+
raise ui.error("No run_id provided")
|
|
22
|
+
|
|
23
|
+
with gql.graphql_client_from_url(url, api_token, deployment_name=deployment) as client:
|
|
24
|
+
try:
|
|
25
|
+
ui.print(gql.run_status(client, run_id))
|
|
26
|
+
except Exception as e:
|
|
27
|
+
ui.error(f"Failed to get status for run {run_id}: {e}.")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# base image is templated in (default: python:3.11-slim)
|
|
2
|
+
|
|
3
|
+
# copy both requirements.txt / setup.py / pyproject.toml to guarantee that at least one file will be copied over
|
|
4
|
+
# for any pre-src COPY. On some flavors of Docker (Linux), the COPY command will fail unless there
|
|
5
|
+
# exists at least one file to copy over.
|
|
6
|
+
COPY *dagster_cloud_pre_install.sh *setup.py *pyproject.toml *requirements.txt /
|
|
7
|
+
RUN if [ -f "dagster_cloud_pre_install.sh" ]; then \
|
|
8
|
+
./dagster_cloud_pre_install.sh; \
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
# This is to enable Docker caching - if setup.py or pyproject.toml exists, we install the
|
|
12
|
+
# dependencies before copying all other files
|
|
13
|
+
RUN if [ -f "setup.py" ]; then \
|
|
14
|
+
pip install .; \
|
|
15
|
+
elif [ -f "pyproject.toml" ]; then \
|
|
16
|
+
pip install .; \
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
WORKDIR /opt/dagster/app
|
|
20
|
+
COPY . /opt/dagster/app
|
|
21
|
+
|
|
22
|
+
# Install the rest of dependencies in case there is a requirements.txt
|
|
23
|
+
# Note: this happens after the src COPY command into the working directory, to ensure that any
|
|
24
|
+
# editable installs will still work
|
|
25
|
+
RUN if [ -f "requirements.txt" ]; then \
|
|
26
|
+
pip install -r requirements.txt; \
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Run the custom dagster cloud post install script if available
|
|
30
|
+
RUN if [ -f "dagster_cloud_post_install.sh" ]; then \
|
|
31
|
+
./dagster_cloud_post_install.sh; \
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Make sure dagster-cloud is installed. Fail early here if not.
|
|
35
|
+
RUN if ! dagster-cloud --version; then \
|
|
36
|
+
echo "Could not find the dagster-cloud package. Make sure you include the dagster-cloud package in your setup.py, pyproject.toml, or requirements.txt file to ensure that it is installed as part of your workspace."; \
|
|
37
|
+
exit 1; \
|
|
38
|
+
fi
|