Skip to content

Commit e3217ee

Browse files
authored
Add ability to convert GitRepository into git_clone deployment step (#10957)
1 parent d6b4f37 commit e3217ee

File tree

8 files changed

+660
-840
lines changed

8 files changed

+660
-840
lines changed

src/prefect/blocks/core.py

+31
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,15 @@ def _should_update_block_type(
182182
return server_block_fields != local_block_fields
183183

184184

185+
class BlockNotSavedError(RuntimeError):
186+
"""
187+
Raised when a given block is not saved and an operation that requires
188+
the block to be saved is attempted.
189+
"""
190+
191+
pass
192+
193+
185194
@register_base_type
186195
@instrument_method_calls_on_class_instances
187196
class Block(BaseModel, ABC):
@@ -1050,3 +1059,25 @@ def __new__(cls: Type[Self], **kwargs) -> Self:
10501059
object.__setattr__(m, "__dict__", kwargs)
10511060
object.__setattr__(m, "__fields_set__", set(kwargs.keys()))
10521061
return m
1062+
1063+
def get_block_placeholder(self) -> str:
1064+
"""
1065+
Returns the block placeholder for the current block which can be used for
1066+
templating.
1067+
1068+
Returns:
1069+
str: The block placeholder for the current block in the format
1070+
`prefect.blocks.{block_type_name}.{block_document_name}`
1071+
1072+
Raises:
1073+
BlockNotSavedError: Raised if the block has not been saved.
1074+
1075+
If a block has not been saved, the return value will be `None`.
1076+
"""
1077+
block_document_name = self._block_document_name
1078+
if not block_document_name:
1079+
raise BlockNotSavedError(
1080+
"Could not generate block placeholder for unsaved block."
1081+
)
1082+
1083+
return f"prefect.blocks.{self.get_block_type_slug()}.{block_document_name}"

src/prefect/deployments/steps/pull.py

+23-133
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
Core set of steps for specifying a Prefect project pull step.
33
"""
44
import os
5-
import subprocess
6-
import sys
7-
import urllib.parse
5+
from pathlib import Path
86
from typing import Optional
97

108
from prefect._internal.compatibility.deprecated import deprecated_callable
119
from prefect.blocks.core import Block
1210
from prefect.logging.loggers import get_logger
11+
from prefect.runner.storage import GitRepository
12+
from prefect.utilities.asyncutils import sync_compatible
1313

1414
deployment_logger = get_logger("deployment")
1515

@@ -29,90 +29,8 @@ def set_working_directory(directory: str) -> dict:
2929
return dict(directory=directory)
3030

3131

32-
def _format_token_from_access_token(netloc: str, access_token: str) -> str:
33-
if "bitbucketserver" in netloc:
34-
if ":" not in access_token:
35-
raise ValueError(
36-
"Please prefix your BitBucket Server access_token with a username,"
37-
" e.g. 'username:token'."
38-
)
39-
# If they pass a header themselves, we can use it as-is
40-
return access_token
41-
42-
elif "bitbucket" in netloc:
43-
return (
44-
access_token
45-
if (access_token.startswith("x-token-auth:") or ":" in access_token)
46-
else f"x-token-auth:{access_token}"
47-
)
48-
49-
elif "gitlab" in netloc:
50-
return (
51-
f"oauth2:{access_token}"
52-
if not access_token.startswith("oauth2:")
53-
else access_token
54-
)
55-
56-
# all other cases (GitHub, etc.)
57-
return access_token
58-
59-
60-
def _format_token_from_credentials(netloc: str, credentials: dict) -> str:
61-
"""
62-
Formats the credentials block for the git provider.
63-
64-
BitBucket supports the following syntax:
65-
git clone "https://x-token-auth:{token}@bitbucket.org/yourRepoOwnerHere/RepoNameHere"
66-
git clone https://username:<token>@bitbucketserver.com/scm/projectname/teamsinspace.git
67-
"""
68-
username = credentials.get("username") if credentials else None
69-
password = credentials.get("password") if credentials else None
70-
token = credentials.get("token") if credentials else None
71-
72-
user_provided_token = token or password
73-
74-
if not user_provided_token:
75-
raise ValueError(
76-
"Please provide a `token` or `password` in your Credentials block to clone"
77-
" a repo."
78-
)
79-
80-
if "bitbucketserver" in netloc:
81-
# If they pass a BitBucketCredentials block and we don't have both a username and at
82-
# least one of a password or token and they don't provide a header themselves,
83-
# we can raise the appropriate error to avoid the wrong format for BitBucket Server.
84-
if not username and ":" not in user_provided_token:
85-
raise ValueError(
86-
"Please provide a `username` and a `password` or `token` in your"
87-
" BitBucketCredentials block to clone a repo from BitBucket Server."
88-
)
89-
# if username or if no username but it's provided in the token
90-
return (
91-
f"{username}:{user_provided_token}"
92-
if username and username not in user_provided_token
93-
else user_provided_token
94-
)
95-
96-
elif "bitbucket" in netloc:
97-
return (
98-
user_provided_token
99-
if user_provided_token.startswith("x-token-auth:")
100-
or ":" in user_provided_token
101-
else f"x-token-auth:{user_provided_token}"
102-
)
103-
104-
elif "gitlab" in netloc:
105-
return (
106-
f"oauth2:{user_provided_token}"
107-
if not user_provided_token.startswith("oauth2:")
108-
else user_provided_token
109-
)
110-
111-
# all other cases (GitHub, etc.)
112-
return user_provided_token
113-
114-
115-
def git_clone(
32+
@sync_compatible
33+
async def git_clone(
11634
repository: str,
11735
branch: Optional[str] = None,
11836
include_submodules: bool = False,
@@ -123,13 +41,13 @@ def git_clone(
12341
Clones a git repository into the current working directory.
12442
12543
Args:
126-
repository (str): the URL of the repository to clone
127-
branch (str, optional): the branch to clone; if not provided, the default branch will be used
44+
repository: the URL of the repository to clone
45+
branch: the branch to clone; if not provided, the default branch will be used
12846
include_submodules (bool): whether to include git submodules when cloning the repository
129-
access_token (str, optional): an access token to use for cloning the repository; if not provided
47+
access_token: an access token to use for cloning the repository; if not provided
13048
the repository will be cloned using the default git credentials
131-
credentials (optional): a GitHubCredentials, GitLabCredentials, or BitBucketCredentials block can be used to specify the
132-
credentials to use for cloning the repository.
49+
credentials: a GitHubCredentials, GitLabCredentials, or BitBucketCredentials block can be used to specify the
50+
credentials to use for cloning the repository.
13351
13452
Returns:
13553
dict: a dictionary containing a `directory` key of the new directory that was created
@@ -193,60 +111,32 @@ def git_clone(
193111
"Please provide either an access token or credentials but not both."
194112
)
195113

196-
url_components = urllib.parse.urlparse(repository)
114+
credentials = {"access_token": access_token} if access_token else credentials
197115

198-
if access_token:
199-
access_token = _format_token_from_access_token(
200-
url_components.netloc, access_token
201-
)
202-
if credentials:
203-
access_token = _format_token_from_credentials(
204-
url_components.netloc, credentials
205-
)
116+
storage = GitRepository(
117+
url=repository,
118+
credentials=credentials,
119+
branch=branch,
120+
include_submodules=include_submodules,
121+
)
206122

207-
if url_components.scheme == "https" and access_token is not None:
208-
updated_components = url_components._replace(
209-
netloc=f"{access_token}@{url_components.netloc}"
210-
)
211-
repository_url = urllib.parse.urlunparse(updated_components)
212-
else:
213-
repository_url = repository
214-
215-
cmd = ["git", "clone", repository_url]
216-
if branch:
217-
cmd += ["-b", branch]
218-
if include_submodules:
219-
cmd += ["--recurse-submodules"]
220-
221-
# Limit git history
222-
cmd += ["--depth", "1"]
223-
224-
try:
225-
subprocess.check_call(
226-
cmd, shell=sys.platform == "win32", stderr=sys.stderr, stdout=sys.stdout
227-
)
228-
except subprocess.CalledProcessError as exc:
229-
# Hide the command used to avoid leaking the access token
230-
exc_chain = None if access_token else exc
231-
raise RuntimeError(
232-
f"Failed to clone repository {repository!r} with exit code"
233-
f" {exc.returncode}."
234-
) from exc_chain
235-
236-
directory = "/".join(repository.strip().split("/")[-1:]).replace(".git", "")
123+
await storage.pull_code()
124+
125+
directory = str(storage.destination.relative_to(Path.cwd()))
237126
deployment_logger.info(f"Cloned repository {repository!r} into {directory!r}")
238127
return {"directory": directory}
239128

240129

241130
@deprecated_callable(start_date="Jun 2023", help="Use 'git clone' instead.")
242-
def git_clone_project(
131+
@sync_compatible
132+
async def git_clone_project(
243133
repository: str,
244134
branch: Optional[str] = None,
245135
include_submodules: bool = False,
246136
access_token: Optional[str] = None,
247137
) -> dict:
248138
"""Deprecated. Use `git_clone` instead."""
249-
return git_clone(
139+
return await git_clone(
250140
repository=repository,
251141
branch=branch,
252142
include_submodules=include_submodules,

0 commit comments

Comments
 (0)