truefoundry 0.4.1__py3-none-any.whl → 0.4.2__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.

Potentially problematic release.


This version of truefoundry might be problematic. Click here for more details.

Files changed (38) hide show
  1. truefoundry/common/auth_service_client.py +14 -5
  2. truefoundry/common/constants.py +2 -1
  3. truefoundry/common/credential_file_manager.py +4 -3
  4. truefoundry/common/credential_provider.py +4 -5
  5. truefoundry/common/servicefoundry_client.py +14 -7
  6. truefoundry/common/utils.py +59 -10
  7. truefoundry/deploy/auto_gen/models.py +11 -5
  8. truefoundry/deploy/builder/__init__.py +2 -2
  9. truefoundry/deploy/builder/builders/tfy_notebook_buildpack/__init__.py +7 -1
  10. truefoundry/deploy/builder/builders/tfy_notebook_buildpack/dockerfile_template.py +25 -12
  11. truefoundry/deploy/builder/builders/tfy_python_buildpack/__init__.py +8 -2
  12. truefoundry/deploy/builder/builders/tfy_python_buildpack/dockerfile_template.py +26 -3
  13. truefoundry/deploy/builder/constants.py +7 -0
  14. truefoundry/deploy/builder/utils.py +32 -0
  15. truefoundry/deploy/cli/commands/deploy_command.py +46 -2
  16. truefoundry/deploy/cli/util.py +4 -1
  17. truefoundry/deploy/lib/auth/servicefoundry_session.py +4 -2
  18. truefoundry/deploy/lib/clients/servicefoundry_client.py +3 -1
  19. truefoundry/deploy/lib/session.py +6 -6
  20. truefoundry/deploy/v2/lib/patched_models.py +4 -0
  21. truefoundry/ml/autogen/client/__init__.py +3 -0
  22. truefoundry/ml/autogen/client/api/mlfoundry_artifacts_api.py +163 -0
  23. truefoundry/ml/autogen/client/models/__init__.py +3 -0
  24. truefoundry/ml/autogen/client/models/delete_files_for_dataset_request_dto.py +68 -0
  25. truefoundry/ml/autogen/client_README.md +2 -0
  26. truefoundry/ml/autogen/entities/artifacts.py +286 -0
  27. truefoundry/ml/clients/servicefoundry_client.py +2 -4
  28. truefoundry/ml/log_types/artifacts/artifact.py +59 -1
  29. truefoundry/ml/mlfoundry_api.py +48 -3
  30. truefoundry/ml/mlfoundry_run.py +33 -16
  31. truefoundry/ml/run_utils.py +0 -14
  32. truefoundry/ml/session.py +9 -8
  33. truefoundry/workflow/example/hello_world_package/workflow.py +2 -2
  34. truefoundry/workflow/example/package/test_workflow.py +14 -15
  35. {truefoundry-0.4.1.dist-info → truefoundry-0.4.2.dist-info}/METADATA +1 -2
  36. {truefoundry-0.4.1.dist-info → truefoundry-0.4.2.dist-info}/RECORD +38 -34
  37. {truefoundry-0.4.1.dist-info → truefoundry-0.4.2.dist-info}/WHEEL +0 -0
  38. {truefoundry-0.4.1.dist-info → truefoundry-0.4.2.dist-info}/entry_points.txt +0 -0
@@ -8,7 +8,7 @@ from truefoundry.common.constants import VERSION_PREFIX
8
8
  from truefoundry.common.entities import DeviceCode, Token
9
9
  from truefoundry.common.exceptions import BadRequestException
10
10
  from truefoundry.common.request_utils import request_handling, requests_retry_session
11
- from truefoundry.common.utils import poll_for_function
11
+ from truefoundry.common.utils import poll_for_function, relogin_error_message
12
12
  from truefoundry.logger import logger
13
13
 
14
14
 
@@ -16,6 +16,7 @@ class AuthServiceClient(ABC):
16
16
  def __init__(self, tenant_name: str):
17
17
  self._tenant_name = tenant_name
18
18
 
19
+ # TODO (chiragjn): Rename base_url to tfy_host
19
20
  @classmethod
20
21
  def from_base_url(cls, base_url: str) -> "AuthServiceClient":
21
22
  from truefoundry.common.servicefoundry_client import (
@@ -54,7 +55,9 @@ class ServiceFoundryServerAuthServiceClient(AuthServiceClient):
54
55
  if not token.refresh_token:
55
56
  # TODO: Add a way to propagate error messages without traceback to the output interface side
56
57
  raise Exception(
57
- f"Unable to resume login session. Please log in again using `tfy login {host_arg_str} --relogin`"
58
+ relogin_error_message(
59
+ "Unable to resume login session.", host=host_arg_str
60
+ )
58
61
  )
59
62
  url = f"{self._api_server_url}/{VERSION_PREFIX}/oauth2/token"
60
63
  data = {
@@ -70,7 +73,9 @@ class ServiceFoundryServerAuthServiceClient(AuthServiceClient):
70
73
  return Token.parse_obj(response_data)
71
74
  except BadRequestException as ex:
72
75
  raise Exception(
73
- f"Unable to resume login session. Please log in again using `tfy login {host_arg_str} --relogin`"
76
+ relogin_error_message(
77
+ "Unable to resume login session.", host=host_arg_str
78
+ )
74
79
  ) from ex
75
80
 
76
81
  def get_device_code(self) -> DeviceCode:
@@ -127,7 +132,9 @@ class AuthServerServiceClient(AuthServiceClient):
127
132
  if not token.refresh_token:
128
133
  # TODO: Add a way to propagate error messages without traceback to the output interface side
129
134
  raise Exception(
130
- f"Unable to resume login session. Please log in again using `tfy login {host_arg_str} --relogin`"
135
+ relogin_error_message(
136
+ "Unable to resume login session.", host=host_arg_str
137
+ )
131
138
  )
132
139
  url = f"{self._auth_server_url}/api/{VERSION_PREFIX}/oauth/token/refresh"
133
140
  data = {
@@ -141,7 +148,9 @@ class AuthServerServiceClient(AuthServiceClient):
141
148
  return Token.parse_obj(response_data)
142
149
  except BadRequestException as ex:
143
150
  raise Exception(
144
- f"Unable to resume login session. Please log in again using `tfy login {host_arg_str} --relogin`"
151
+ relogin_error_message(
152
+ "Unable to resume login session.", host=host_arg_str
153
+ )
145
154
  ) from ex
146
155
 
147
156
  def get_device_code(self) -> DeviceCode:
@@ -5,8 +5,9 @@ CREDENTIAL_FILEPATH = TFY_CONFIG_DIR / "credentials.json"
5
5
 
6
6
  TFY_HOST_ENV_KEY = "TFY_HOST"
7
7
  TFY_API_KEY_ENV_KEY = "TFY_API_KEY"
8
- SERVICEFOUNDRY_SERVER_URL_ENV_KEY = "SERVICEFOUNDRY_SERVER_URL"
8
+ TFY_CLI_LOCAL_DEV_MODE_ENV_KEY = "TFY_CLI_LOCAL_DEV_MODE"
9
9
 
10
10
  API_SERVER_RELATIVE_PATH = "api/svc"
11
+ MLFOUNDRY_SERVER_RELATIVE_PATH = "api/ml"
11
12
  VERSION_PREFIX = "v1"
12
13
  SERVICEFOUNDRY_CLIENT_MAX_RETRIES = 2
@@ -10,6 +10,7 @@ from filelock import FileLock, Timeout
10
10
 
11
11
  from truefoundry.common.constants import CREDENTIAL_FILEPATH
12
12
  from truefoundry.common.entities import CredentialsFileContent
13
+ from truefoundry.common.utils import relogin_error_message
13
14
  from truefoundry.logger import logger
14
15
 
15
16
 
@@ -87,9 +88,9 @@ class CredentialsFileManager:
87
88
  return CredentialsFileContent.parse_file(self._credentials_file_path)
88
89
  except Exception as ex:
89
90
  raise Exception(
90
- "Error while reading the credentials file "
91
- f"{self._credentials_file_path}. Please login again "
92
- "using `tfy login --relogin` or `tfy.login(relogin=True)` function"
91
+ relogin_error_message(
92
+ f"Error while reading the credentials file {self._credentials_file_path}"
93
+ )
93
94
  ) from ex
94
95
 
95
96
  @_ensure_lock_taken
@@ -6,7 +6,7 @@ from truefoundry.common.auth_service_client import AuthServiceClient
6
6
  from truefoundry.common.constants import TFY_API_KEY_ENV_KEY
7
7
  from truefoundry.common.credential_file_manager import CredentialsFileManager
8
8
  from truefoundry.common.entities import CredentialsFileContent, Token
9
- from truefoundry.common.utils import resolve_base_url
9
+ from truefoundry.common.utils import resolve_tfy_host
10
10
  from truefoundry.logger import logger
11
11
 
12
12
  TOKEN_REFRESH_LOCK = threading.RLock()
@@ -17,6 +17,7 @@ class CredentialProvider(ABC):
17
17
  @abstractmethod
18
18
  def token(self) -> Token: ...
19
19
 
20
+ # TODO (chiragjn): Rename base_url to tfy_host
20
21
  @property
21
22
  @abstractmethod
22
23
  def base_url(self) -> str: ...
@@ -34,10 +35,8 @@ class EnvCredentialProvider(CredentialProvider):
34
35
  raise Exception(
35
36
  f"Value of {TFY_API_KEY_ENV_KEY} env var should be non-empty string"
36
37
  )
37
- # TODO: Read host from cred file as well.
38
- base_url = resolve_base_url().strip("/")
39
- self._host = base_url
40
- self._auth_service = AuthServiceClient.from_base_url(base_url=base_url)
38
+ self._host = resolve_tfy_host()
39
+ self._auth_service = AuthServiceClient.from_base_url(base_url=self._host)
41
40
  self._token: Token = Token(access_token=api_key, refresh_token=None) # type: ignore[call-arg]
42
41
 
43
42
  @staticmethod
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import functools
4
- from urllib.parse import urlparse
5
4
 
6
5
  import requests
7
6
  from packaging import version
@@ -16,7 +15,7 @@ from truefoundry.common.entities import (
16
15
  )
17
16
  from truefoundry.common.request_utils import request_handling, requests_retry_session
18
17
  from truefoundry.common.utils import (
19
- append_servicefoundry_path_to_base_url,
18
+ get_tfy_servers_config,
20
19
  timed_lru_cache,
21
20
  )
22
21
  from truefoundry.logger import logger
@@ -49,10 +48,10 @@ def session_with_retries() -> requests.Session:
49
48
 
50
49
 
51
50
  @timed_lru_cache(seconds=30 * 60)
52
- def _cached_get_tenant_info(api_server_url: str) -> TenantInfo:
51
+ def _cached_get_tenant_info(api_server_url: str, tenant_host: str) -> TenantInfo:
53
52
  res = session_with_retries().get(
54
53
  url=f"{api_server_url}/{VERSION_PREFIX}/tenant-id",
55
- params={"hostName": urlparse(api_server_url).netloc},
54
+ params={"hostName": tenant_host},
56
55
  )
57
56
  res = request_handling(res)
58
57
  return TenantInfo.parse_obj(res)
@@ -68,9 +67,12 @@ def _cached_get_python_sdk_config(api_server_url: str) -> PythonSDKConfig:
68
67
 
69
68
 
70
69
  class ServiceFoundryServiceClient:
70
+ # TODO (chiragjn): Rename base_url to tfy_host
71
71
  def __init__(self, base_url: str):
72
72
  self._base_url = base_url.strip("/")
73
- self._api_server_url = append_servicefoundry_path_to_base_url(self._base_url)
73
+ tfy_servers_config = get_tfy_servers_config(self._base_url)
74
+ self._tenant_host = tfy_servers_config.tenant_host
75
+ self._api_server_url = tfy_servers_config.servicefoundry_server_url
74
76
 
75
77
  @property
76
78
  def base_url(self) -> str:
@@ -78,11 +80,16 @@ class ServiceFoundryServiceClient:
78
80
 
79
81
  @property
80
82
  def tenant_info(self) -> TenantInfo:
81
- return _cached_get_tenant_info(self._api_server_url)
83
+ return _cached_get_tenant_info(
84
+ self._api_server_url,
85
+ tenant_host=self._tenant_host,
86
+ )
82
87
 
83
88
  @property
84
89
  def python_sdk_config(self) -> PythonSDKConfig:
85
- return _cached_get_python_sdk_config(self._api_server_url)
90
+ return _cached_get_python_sdk_config(
91
+ self._api_server_url,
92
+ )
86
93
 
87
94
  @functools.cached_property
88
95
  def _min_cli_version_required(self) -> str:
@@ -3,17 +3,62 @@ import time
3
3
  from functools import lru_cache, wraps
4
4
  from time import monotonic_ns
5
5
  from typing import Callable, Generator, Optional, TypeVar
6
- from urllib.parse import urljoin, urlsplit
6
+ from urllib.parse import urljoin, urlparse
7
7
 
8
8
  from truefoundry.common.constants import (
9
9
  API_SERVER_RELATIVE_PATH,
10
- SERVICEFOUNDRY_SERVER_URL_ENV_KEY,
10
+ MLFOUNDRY_SERVER_RELATIVE_PATH,
11
+ TFY_CLI_LOCAL_DEV_MODE_ENV_KEY,
11
12
  TFY_HOST_ENV_KEY,
12
13
  )
14
+ from truefoundry.pydantic_v1 import BaseSettings
13
15
 
14
16
  T = TypeVar("T")
15
17
 
16
18
 
19
+ class _TFYServersConfig(BaseSettings):
20
+ class Config:
21
+ env_prefix = "TFY_CLI_LOCAL_"
22
+ env_file = ".tfy-cli-local.env"
23
+
24
+ tenant_host: str
25
+ servicefoundry_server_url: str
26
+ mlfoundry_server_url: str
27
+
28
+ @classmethod
29
+ def from_base_url(cls, base_url: str) -> "_TFYServersConfig":
30
+ base_url = base_url.strip("/")
31
+ return cls(
32
+ tenant_host=urlparse(base_url).netloc,
33
+ servicefoundry_server_url=urljoin(base_url, API_SERVER_RELATIVE_PATH),
34
+ mlfoundry_server_url=urljoin(base_url, MLFOUNDRY_SERVER_RELATIVE_PATH),
35
+ )
36
+
37
+
38
+ _tfy_servers_config = None
39
+
40
+
41
+ def get_tfy_servers_config(base_url: str) -> _TFYServersConfig:
42
+ global _tfy_servers_config
43
+ if _tfy_servers_config is None:
44
+ if os.getenv(TFY_CLI_LOCAL_DEV_MODE_ENV_KEY):
45
+ _tfy_servers_config = _TFYServersConfig() # type: ignore[call-arg]
46
+ else:
47
+ _tfy_servers_config = _TFYServersConfig.from_base_url(base_url)
48
+ return _tfy_servers_config
49
+
50
+
51
+ def relogin_error_message(message: str, host: str = "HOST") -> str:
52
+ suffix = ""
53
+ if host == "HOST":
54
+ suffix = " where HOST is TrueFoundry platform URL"
55
+ return (
56
+ f"{message}\n"
57
+ f"Please login again using `tfy login --host {host} --relogin` "
58
+ f"or `truefoundry.login(host={host!r}, relogin=True)` function" + suffix
59
+ )
60
+
61
+
17
62
  def timed_lru_cache(
18
63
  seconds: int = 300, maxsize: Optional[int] = None
19
64
  ) -> Callable[[Callable[..., T]], Callable[..., T]]:
@@ -42,15 +87,19 @@ def poll_for_function(
42
87
  time.sleep(poll_after_secs)
43
88
 
44
89
 
45
- def resolve_base_url(host: Optional[str] = None) -> str:
46
- if not host and not os.getenv(TFY_HOST_ENV_KEY):
90
+ def validate_tfy_host(tfy_host: str) -> None:
91
+ if not (tfy_host.startswith("https://") or tfy_host.startswith("http://")):
47
92
  raise ValueError(
48
- f"Either `host` should be provided by --host <value>, or `{TFY_HOST_ENV_KEY}` env must be set"
93
+ f"Invalid host {tfy_host!r}. It should start with https:// or http://"
49
94
  )
50
- return host or os.getenv(TFY_HOST_ENV_KEY)
51
95
 
52
96
 
53
- def append_servicefoundry_path_to_base_url(base_url: str):
54
- if urlsplit(base_url).netloc.startswith("localhost"):
55
- return os.getenv(SERVICEFOUNDRY_SERVER_URL_ENV_KEY)
56
- return urljoin(base_url, API_SERVER_RELATIVE_PATH)
97
+ def resolve_tfy_host(tfy_host: Optional[str] = None) -> str:
98
+ if not tfy_host and not os.getenv(TFY_HOST_ENV_KEY):
99
+ raise ValueError(
100
+ f"Either `host` should be provided using `--host <value>`, or `{TFY_HOST_ENV_KEY}` env must be set"
101
+ )
102
+ tfy_host = tfy_host or os.getenv(TFY_HOST_ENV_KEY)
103
+ tfy_host = tfy_host.strip("/")
104
+ validate_tfy_host(tfy_host)
105
+ return tfy_host
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: application.json
3
- # timestamp: 2024-09-29T10:28:22+00:00
3
+ # timestamp: 2024-10-09T13:00:44+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -363,10 +363,12 @@ class HelmRepo(BaseModel):
363
363
  description="+label=Helm repository URL\n+sort=1\n+message=Needs to be a valid URL.",
364
364
  )
365
365
  chart: str = Field(
366
- ..., description="+label=Chart name\n+sort=2\n+usage=The helm chart name"
366
+ ...,
367
+ description='+label=Chart name\n+sort=2\n+usage=The helm chart name\n+uiType=InputSelect\n+uiProps={"creatable":true, "searchable":true}',
367
368
  )
368
369
  version: str = Field(
369
- ..., description="+label=Version\n+sort=3\n+usage=Helm chart version"
370
+ ...,
371
+ description='+label=Version\n+sort=3\n+usage=Helm chart version\n+uiType=InputSelect\n+uiProps={"creatable":true, "searchable":true}',
370
372
  )
371
373
 
372
374
 
@@ -448,10 +450,14 @@ class JobAlert(BaseModel):
448
450
  +label=Alert
449
451
  """
450
452
 
451
- notification_channel: str = Field(
453
+ notification_channel: constr(min_length=1) = Field(
452
454
  ...,
453
455
  description='+label=Notification Channel\n+usage=Specify the notification channel to send alerts to\n+uiType=IntegrationSelect\n+uiProps={"integrationType":"notification-channel"}\n+sort=660',
454
456
  )
457
+ to_emails: Optional[List[constr(min_length=1)]] = Field(
458
+ None,
459
+ description="+label=To Emails\n+usage=List of recipients' email addresses if the notification channel is Email.\n+docs=Specify the emails to send alerts to\n+sort=665",
460
+ )
455
461
  on_start: bool = Field(
456
462
  False,
457
463
  description="+label=On Start\n+usage=Send an alert when the job starts\n+sort=670",
@@ -1546,7 +1552,7 @@ class SSHServer(BaseWorkbenchInput):
1546
1552
  image: WorkbenchImage
1547
1553
  ssh_public_key: str = Field(
1548
1554
  ...,
1549
- description="+label: SSH Public Key\n+usage=Add Your SSH Public Key, this will be used to authenticate you to the SSH Server. You can find it using `cat ~/.ssh/id_rsa.pub`\n+sort=4",
1555
+ description="+label: SSH Public Key\n+usage=Add Your SSH Public Key, this will be used to authenticate you to the SSH Server. \\\nYou can find it using `cat ~/.ssh/id_rsa.pub` in Mac/Linux or `type $home\\.ssh\\id_rsa.pub` in Windows Powershell. \\\nYou can also generate a new SSH key pair using `ssh-keygen -t rsa` in your local terminal. (same for both Mac/Linux and Windows Powershell)\n+uiType=TextArea\n+sort=4",
1550
1556
  )
1551
1557
 
1552
1558
 
@@ -1,4 +1,4 @@
1
- from typing import Dict, List, Optional, Union
1
+ from typing import Any, Dict, List, Optional, Union
2
2
 
3
3
  from truefoundry.deploy.auto_gen.models import (
4
4
  DockerFileBuild,
@@ -24,7 +24,7 @@ class _BuildConfig(BaseModel):
24
24
 
25
25
 
26
26
  def build(
27
- build_configuration: Union[BaseModel, Dict],
27
+ build_configuration: Union[BaseModel, Dict[str, Any]],
28
28
  tag: str,
29
29
  extra_opts: Optional[List[str]] = None,
30
30
  ):
@@ -8,16 +8,20 @@ from truefoundry.deploy.builder.builders.tfy_notebook_buildpack.dockerfile_templ
8
8
  NotebookImageBuild,
9
9
  generate_dockerfile_content,
10
10
  )
11
+ from truefoundry.deploy.builder.utils import has_pip_conf_secret
11
12
 
12
13
  __all__ = ["generate_dockerfile_content", "build"]
13
14
 
14
15
 
15
16
  def _convert_to_dockerfile_build_config(
16
- build_configuration: NotebookImageBuild, local_dir: str
17
+ build_configuration: NotebookImageBuild,
18
+ local_dir: str,
19
+ mount_pip_conf_secret: bool = False,
17
20
  ) -> DockerFileBuild:
18
21
  dockerfile_content = generate_dockerfile_content(
19
22
  build_configuration=build_configuration,
20
23
  local_dir=local_dir,
24
+ mount_pip_conf_secret=mount_pip_conf_secret,
21
25
  )
22
26
  dockerfile_path = os.path.join(local_dir, "Dockerfile")
23
27
  with open(dockerfile_path, "w", encoding="utf8") as fp:
@@ -34,10 +38,12 @@ def build(
34
38
  build_configuration: NotebookImageBuild,
35
39
  extra_opts: Optional[List[str]] = None,
36
40
  ):
41
+ mount_pip_conf_secret = has_pip_conf_secret(extra_opts) if extra_opts else False
37
42
  with TemporaryDirectory() as local_dir:
38
43
  docker_build_configuration = _convert_to_dockerfile_build_config(
39
44
  build_configuration,
40
45
  local_dir=local_dir,
46
+ mount_pip_conf_secret=mount_pip_conf_secret,
41
47
  )
42
48
  dockerfile.build(
43
49
  tag=tag,
@@ -4,6 +4,10 @@ from typing import Literal, Optional
4
4
 
5
5
  from mako.template import Template
6
6
 
7
+ from truefoundry.deploy.builder.constants import (
8
+ PIP_CONF_BUILDKIT_SECRET_MOUNT,
9
+ PIP_CONF_SECRET_MOUNT_AS_ENV,
10
+ )
7
11
  from truefoundry.pydantic_v1 import BaseModel
8
12
 
9
13
 
@@ -28,31 +32,40 @@ USER $NB_UID
28
32
 
29
33
 
30
34
  def generate_build_script_docker_commands(
31
- build_script: Optional[str], local_dir: str
35
+ build_script: Optional[str],
36
+ local_dir: str,
37
+ mount_pip_conf_secret: bool = False,
32
38
  ) -> Optional[str]:
33
39
  if not build_script:
34
40
  return None
35
- build_script_path = None
36
- if build_script:
37
- # we add build script's hash to the file name to ensure docker cache invalidation
38
- script_hash = hashlib.sha256(build_script.encode("utf-8")).hexdigest()
39
- build_script_path = os.path.join(local_dir, f"build-script-{script_hash}.sh")
40
- with open(build_script_path, "w") as fp:
41
- fp.write(build_script)
42
- build_script_path = os.path.relpath(build_script_path, local_dir)
41
+ # we add build script's hash to the file name to ensure docker cache invalidation
42
+ script_hash = hashlib.sha256(build_script.encode("utf-8")).hexdigest()
43
+ build_script_path = os.path.join(local_dir, f"build-script-{script_hash}.sh")
44
+ with open(build_script_path, "w") as fp:
45
+ fp.write(build_script)
46
+ build_script_path = os.path.relpath(build_script_path, local_dir)
47
+ buildkit_secret_mounts = ""
48
+ environment_variables = ["DEBIAN_FRONTEND=noninteractive"]
49
+ if mount_pip_conf_secret:
50
+ buildkit_secret_mounts = PIP_CONF_BUILDKIT_SECRET_MOUNT
51
+ environment_variables.append(PIP_CONF_SECRET_MOUNT_AS_ENV)
52
+ all_environment_variables = " ".join(environment_variables)
43
53
  run_build_script_command = f"""\
44
54
  COPY {build_script_path} /tmp/user-build-script.sh
45
- RUN mkdir -p /var/log/ && DEBIAN_FRONTEND=noninteractive bash -ex /tmp/user-build-script.sh 2>&1 | tee /var/log/user-build-script-output.log
55
+ RUN {buildkit_secret_mounts} mkdir -p /var/log/ && {all_environment_variables} bash -ex /tmp/user-build-script.sh 2>&1 | tee /var/log/user-build-script-output.log
46
56
  """
47
57
  return run_build_script_command
48
58
 
49
59
 
50
60
  def generate_dockerfile_content(
51
- build_configuration: NotebookImageBuild, local_dir: str
61
+ build_configuration: NotebookImageBuild,
62
+ local_dir: str,
63
+ mount_pip_conf_secret: bool = False,
52
64
  ) -> str:
53
65
  build_script_docker_commands = generate_build_script_docker_commands(
54
66
  build_script=build_configuration.build_script,
55
67
  local_dir=local_dir,
68
+ mount_pip_conf_secret=mount_pip_conf_secret,
56
69
  )
57
70
 
58
71
  template_args = {
@@ -62,5 +75,5 @@ def generate_dockerfile_content(
62
75
 
63
76
  template = DOCKERFILE_TEMPLATE
64
77
 
65
- dockerfile_content = template.render(**template_args)
78
+ dockerfile_content: str = template.render(**template_args)
66
79
  return dockerfile_content
@@ -7,6 +7,7 @@ from truefoundry.deploy.builder.builders import dockerfile
7
7
  from truefoundry.deploy.builder.builders.tfy_python_buildpack.dockerfile_template import (
8
8
  generate_dockerfile_content,
9
9
  )
10
+ from truefoundry.deploy.builder.utils import has_pip_conf_secret
10
11
 
11
12
  __all__ = ["generate_dockerfile_content", "build"]
12
13
 
@@ -14,9 +15,11 @@ __all__ = ["generate_dockerfile_content", "build"]
14
15
  def _convert_to_dockerfile_build_config(
15
16
  build_configuration: PythonBuild,
16
17
  dockerfile_path: str,
18
+ mount_pip_conf_secret: bool = False,
17
19
  ) -> DockerFileBuild:
18
20
  dockerfile_content = generate_dockerfile_content(
19
- build_configuration=build_configuration
21
+ build_configuration=build_configuration,
22
+ mount_pip_conf_secret=mount_pip_conf_secret,
20
23
  )
21
24
  with open(dockerfile_path, "w", encoding="utf8") as fp:
22
25
  fp.write(dockerfile_content)
@@ -33,9 +36,12 @@ def build(
33
36
  build_configuration: PythonBuild,
34
37
  extra_opts: Optional[List[str]] = None,
35
38
  ):
39
+ mount_pip_conf_secret = has_pip_conf_secret(extra_opts) if extra_opts else False
36
40
  with TemporaryDirectory() as local_dir:
37
41
  docker_build_configuration = _convert_to_dockerfile_build_config(
38
- build_configuration, dockerfile_path=os.path.join(local_dir, "Dockerfile")
42
+ build_configuration,
43
+ dockerfile_path=os.path.join(local_dir, "Dockerfile"),
44
+ mount_pip_conf_secret=mount_pip_conf_secret,
39
45
  )
40
46
  dockerfile.build(
41
47
  tag=tag,
@@ -4,6 +4,10 @@ from typing import Dict, List, Optional
4
4
  from mako.template import Template
5
5
 
6
6
  from truefoundry.deploy.auto_gen.models import PythonBuild
7
+ from truefoundry.deploy.builder.constants import (
8
+ PIP_CONF_BUILDKIT_SECRET_MOUNT,
9
+ PIP_CONF_SECRET_MOUNT_AS_ENV,
10
+ )
7
11
  from truefoundry.deploy.v2.lib.patched_models import CUDAVersion
8
12
 
9
13
  # TODO (chiragjn): Switch to a non-root user inside the container
@@ -18,7 +22,7 @@ COPY ${requirements_path} ${requirements_destination_path}
18
22
  % endif
19
23
 
20
24
  % if pip_install_command is not None:
21
- RUN ${pip_install_command}
25
+ RUN ${pip_config_secret_mount} ${pip_install_command}
22
26
  % endif
23
27
 
24
28
  COPY . /app
@@ -27,7 +31,7 @@ WORKDIR /app
27
31
 
28
32
  DOCKERFILE_TEMPLATE = Template(
29
33
  """
30
- FROM --platform=linux/amd64 python:${python_version}
34
+ FROM --platform=linux/amd64 public.ecr.aws/docker/library/python:${python_version}
31
35
  ENV PATH=/virtualenvs/venv/bin:$PATH
32
36
  RUN apt update && \
33
37
  DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends git && \
@@ -65,6 +69,11 @@ CUDA_VERSION_TO_IMAGE_TAG: Dict[str, str] = {
65
69
  CUDAVersion.CUDA_12_0_CUDNN8.value: "12.0.1-cudnn8-runtime-ubuntu22.04",
66
70
  CUDAVersion.CUDA_12_1_CUDNN8.value: "12.1.1-cudnn8-runtime-ubuntu22.04",
67
71
  CUDAVersion.CUDA_12_2_CUDNN8.value: "12.2.2-cudnn8-runtime-ubuntu22.04",
72
+ CUDAVersion.CUDA_12_3_CUDNN9.value: "12.3.2-cudnn9-runtime-ubuntu22.04",
73
+ # From 12.4+ onwards, the image tags drop the cudnn version
74
+ CUDAVersion.CUDA_12_4_CUDNN9.value: "12.4.1-cudnn-runtime-ubuntu22.04",
75
+ CUDAVersion.CUDA_12_5_CUDNN9.value: "12.5.1-cudnn-runtime-ubuntu22.04",
76
+ CUDAVersion.CUDA_12_6_CUDNN9.value: "12.6.1-cudnn-runtime-ubuntu22.04",
68
77
  }
69
78
 
70
79
 
@@ -100,7 +109,9 @@ def generate_apt_install_command(apt_packages: Optional[List[str]]) -> Optional[
100
109
 
101
110
 
102
111
  def generate_pip_install_command(
103
- requirements_path: Optional[str], pip_packages: Optional[List[str]]
112
+ requirements_path: Optional[str],
113
+ pip_packages: Optional[List[str]],
114
+ mount_pip_conf_secret: bool = False,
104
115
  ) -> Optional[str]:
105
116
  upgrade_pip_command = "python -m pip install -U pip setuptools wheel"
106
117
  final_pip_install_command = None
@@ -119,11 +130,17 @@ def generate_pip_install_command(
119
130
  if not final_pip_install_command:
120
131
  return None
121
132
 
133
+ if mount_pip_conf_secret:
134
+ final_pip_install_command = (
135
+ f"{PIP_CONF_SECRET_MOUNT_AS_ENV} {final_pip_install_command}"
136
+ )
137
+
122
138
  return " && ".join([upgrade_pip_command, final_pip_install_command])
123
139
 
124
140
 
125
141
  def generate_dockerfile_content(
126
142
  build_configuration: PythonBuild,
143
+ mount_pip_conf_secret: bool = False,
127
144
  ) -> str:
128
145
  # TODO (chiragjn): Handle recursive references to other requirements files e.g. `-r requirements-gpu.txt`
129
146
  requirements_path = resolve_requirements_txt_path(build_configuration)
@@ -133,6 +150,7 @@ def generate_dockerfile_content(
133
150
  pip_install_command = generate_pip_install_command(
134
151
  requirements_path=requirements_destination_path,
135
152
  pip_packages=build_configuration.pip_packages,
153
+ mount_pip_conf_secret=mount_pip_conf_secret,
136
154
  )
137
155
  apt_install_command = generate_apt_install_command(
138
156
  apt_packages=build_configuration.apt_packages
@@ -146,6 +164,11 @@ def generate_dockerfile_content(
146
164
  "pip_install_command": pip_install_command,
147
165
  }
148
166
 
167
+ if mount_pip_conf_secret:
168
+ template_args["pip_config_secret_mount"] = PIP_CONF_BUILDKIT_SECRET_MOUNT
169
+ else:
170
+ template_args["pip_config_secret_mount"] = ""
171
+
149
172
  if build_configuration.cuda_version:
150
173
  template = CUDA_DOCKERFILE_TEMPLATE
151
174
  template_args["nvidia_cuda_image_tag"] = CUDA_VERSION_TO_IMAGE_TAG.get(
@@ -0,0 +1,7 @@
1
+ BUILDKIT_SECRET_MOUNT_PIP_CONF_ID = "pip.conf"
2
+ PIP_CONF_BUILDKIT_SECRET_MOUNT = (
3
+ f"--mount=type=secret,id={BUILDKIT_SECRET_MOUNT_PIP_CONF_ID}"
4
+ )
5
+ PIP_CONF_SECRET_MOUNT_AS_ENV = (
6
+ f"PIP_CONFIG_FILE=/run/secrets/{BUILDKIT_SECRET_MOUNT_PIP_CONF_ID}"
7
+ )
@@ -0,0 +1,32 @@
1
+ from typing import List, Optional
2
+
3
+ from truefoundry.deploy.builder.constants import BUILDKIT_SECRET_MOUNT_PIP_CONF_ID
4
+
5
+
6
+ def _get_id_from_buildkit_secret_value(value: str) -> Optional[str]:
7
+ parts = value.split(",")
8
+ secret_config = {}
9
+ for part in parts:
10
+ kv = part.split("=", 1)
11
+ if len(kv) != 2:
12
+ continue
13
+ key, value = kv
14
+ secret_config[key] = value
15
+
16
+ if "id" in secret_config and "src" in secret_config:
17
+ return secret_config["id"]
18
+
19
+ return None
20
+
21
+
22
+ def has_pip_conf_secret(docker_build_extra_args: List[str]) -> bool:
23
+ args = [arg.strip() for arg in docker_build_extra_args]
24
+ for i, arg in enumerate(docker_build_extra_args):
25
+ if (
26
+ arg == "--secret"
27
+ and i + 1 < len(args)
28
+ and _get_id_from_buildkit_secret_value(args[i + 1])
29
+ == BUILDKIT_SECRET_MOUNT_PIP_CONF_ID
30
+ ):
31
+ return True
32
+ return False