truefoundry 0.2.10__py3-none-any.whl → 0.3.0__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.
- truefoundry/__init__.py +1 -0
- truefoundry/autodeploy/cli.py +31 -18
- truefoundry/deploy/__init__.py +112 -1
- truefoundry/deploy/auto_gen/models.py +1714 -0
- truefoundry/deploy/builder/__init__.py +134 -0
- truefoundry/deploy/builder/builders/__init__.py +22 -0
- truefoundry/deploy/builder/builders/dockerfile.py +57 -0
- truefoundry/deploy/builder/builders/tfy_notebook_buildpack/__init__.py +46 -0
- truefoundry/deploy/builder/builders/tfy_notebook_buildpack/dockerfile_template.py +66 -0
- truefoundry/deploy/builder/builders/tfy_python_buildpack/__init__.py +44 -0
- truefoundry/deploy/builder/builders/tfy_python_buildpack/dockerfile_template.py +158 -0
- truefoundry/deploy/builder/docker_service.py +168 -0
- truefoundry/deploy/cli/cli.py +21 -26
- truefoundry/deploy/cli/commands/__init__.py +18 -0
- truefoundry/deploy/cli/commands/apply_command.py +52 -0
- truefoundry/deploy/cli/commands/build_command.py +45 -0
- truefoundry/deploy/cli/commands/build_logs_command.py +89 -0
- truefoundry/deploy/cli/commands/create_command.py +75 -0
- truefoundry/deploy/cli/commands/delete_command.py +77 -0
- truefoundry/deploy/cli/commands/deploy_command.py +102 -0
- truefoundry/deploy/cli/commands/get_command.py +216 -0
- truefoundry/deploy/cli/commands/list_command.py +171 -0
- truefoundry/deploy/cli/commands/login_command.py +33 -0
- truefoundry/deploy/cli/commands/logout_command.py +20 -0
- truefoundry/deploy/cli/commands/logs_command.py +134 -0
- truefoundry/deploy/cli/commands/patch_application_command.py +81 -0
- truefoundry/deploy/cli/commands/patch_command.py +70 -0
- truefoundry/deploy/cli/commands/redeploy_command.py +41 -0
- truefoundry/deploy/cli/commands/terminate_comand.py +44 -0
- truefoundry/deploy/cli/commands/trigger_command.py +145 -0
- truefoundry/deploy/cli/config.py +10 -0
- truefoundry/deploy/cli/console.py +5 -0
- truefoundry/deploy/cli/const.py +12 -0
- truefoundry/deploy/cli/display_util.py +118 -0
- truefoundry/deploy/cli/util.py +129 -0
- truefoundry/deploy/core/__init__.py +7 -0
- truefoundry/deploy/core/login.py +9 -0
- truefoundry/deploy/core/logout.py +5 -0
- truefoundry/deploy/function_service/__init__.py +3 -0
- truefoundry/deploy/function_service/__main__.py +27 -0
- truefoundry/deploy/function_service/app.py +92 -0
- truefoundry/deploy/function_service/build.py +45 -0
- truefoundry/deploy/function_service/remote/__init__.py +6 -0
- truefoundry/deploy/function_service/remote/context.py +3 -0
- truefoundry/deploy/function_service/remote/method.py +67 -0
- truefoundry/deploy/function_service/remote/remote.py +144 -0
- truefoundry/deploy/function_service/route.py +137 -0
- truefoundry/deploy/function_service/service.py +113 -0
- truefoundry/deploy/function_service/utils.py +53 -0
- truefoundry/deploy/io/__init__.py +0 -0
- truefoundry/deploy/io/output_callback.py +23 -0
- truefoundry/deploy/io/rich_output_callback.py +27 -0
- truefoundry/deploy/json_util.py +7 -0
- truefoundry/deploy/lib/__init__.py +0 -0
- truefoundry/deploy/lib/auth/auth_service_client.py +181 -0
- truefoundry/deploy/lib/auth/credential_file_manager.py +115 -0
- truefoundry/deploy/lib/auth/credential_provider.py +131 -0
- truefoundry/deploy/lib/auth/servicefoundry_session.py +59 -0
- truefoundry/deploy/lib/clients/__init__.py +0 -0
- truefoundry/deploy/lib/clients/servicefoundry_client.py +746 -0
- truefoundry/deploy/lib/clients/shell_client.py +13 -0
- truefoundry/deploy/lib/clients/utils.py +41 -0
- truefoundry/deploy/lib/const.py +43 -0
- truefoundry/deploy/lib/dao/__init__.py +0 -0
- truefoundry/deploy/lib/dao/application.py +263 -0
- truefoundry/deploy/lib/dao/apply.py +80 -0
- truefoundry/deploy/lib/dao/version.py +33 -0
- truefoundry/deploy/lib/dao/workspace.py +71 -0
- truefoundry/deploy/lib/exceptions.py +26 -0
- truefoundry/deploy/lib/logs_utils.py +43 -0
- truefoundry/deploy/lib/messages.py +12 -0
- truefoundry/deploy/lib/model/__init__.py +0 -0
- truefoundry/deploy/lib/model/entity.py +400 -0
- truefoundry/deploy/lib/session.py +158 -0
- truefoundry/deploy/lib/util.py +90 -0
- truefoundry/deploy/lib/win32.py +129 -0
- truefoundry/deploy/v2/__init__.py +0 -0
- truefoundry/deploy/v2/lib/__init__.py +3 -0
- truefoundry/deploy/v2/lib/deploy.py +283 -0
- truefoundry/deploy/v2/lib/deploy_workflow.py +295 -0
- truefoundry/deploy/v2/lib/deployable_patched_models.py +86 -0
- truefoundry/deploy/v2/lib/models.py +53 -0
- truefoundry/deploy/v2/lib/patched_models.py +479 -0
- truefoundry/deploy/v2/lib/source.py +267 -0
- truefoundry/langchain/__init__.py +12 -1
- truefoundry/langchain/deprecated.py +302 -0
- truefoundry/langchain/truefoundry_chat.py +130 -0
- truefoundry/langchain/truefoundry_embeddings.py +171 -0
- truefoundry/langchain/truefoundry_llm.py +106 -0
- truefoundry/langchain/utils.py +85 -0
- truefoundry/logger.py +17 -0
- truefoundry/pydantic_v1.py +5 -0
- truefoundry/python_deploy_codegen.py +132 -0
- truefoundry/version.py +6 -0
- truefoundry/workflow/__init__.py +19 -0
- truefoundry/workflow/container_task.py +12 -0
- truefoundry/workflow/example/deploy.sh +1 -0
- truefoundry/workflow/example/hello_world_package/workflow.py +20 -0
- truefoundry/workflow/example/package/test_workflow.py +152 -0
- truefoundry/workflow/example/truefoundry.yaml +9 -0
- truefoundry/workflow/example/workflow.yaml +116 -0
- truefoundry/workflow/map_task.py +45 -0
- truefoundry/workflow/python_task.py +32 -0
- truefoundry/workflow/task.py +50 -0
- truefoundry/workflow/workflow.py +114 -0
- {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/METADATA +27 -7
- truefoundry-0.3.0.dist-info/RECORD +136 -0
- truefoundry/deploy/cli/deploy.py +0 -165
- truefoundry/deploy/cli/version.py +0 -6
- truefoundry-0.2.10.dist-info/RECORD +0 -38
- {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/WHEEL +0 -0
- {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
from truefoundry.deploy.auto_gen.models import (
|
|
4
|
+
DockerFileBuild,
|
|
5
|
+
PythonBuild,
|
|
6
|
+
TaskDockerFileBuild,
|
|
7
|
+
TaskPythonBuild,
|
|
8
|
+
)
|
|
9
|
+
from truefoundry.deploy.builder.builders import get_builder
|
|
10
|
+
from truefoundry.deploy.builder.builders.tfy_notebook_buildpack.dockerfile_template import (
|
|
11
|
+
NotebookImageBuild,
|
|
12
|
+
)
|
|
13
|
+
from truefoundry.pydantic_v1 import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _BuildConfig(BaseModel):
|
|
17
|
+
__root__: Union[
|
|
18
|
+
DockerFileBuild,
|
|
19
|
+
PythonBuild,
|
|
20
|
+
NotebookImageBuild,
|
|
21
|
+
TaskPythonBuild,
|
|
22
|
+
TaskDockerFileBuild,
|
|
23
|
+
] = Field(discriminator="type")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build(
|
|
27
|
+
build_configuration: Union[BaseModel, Dict],
|
|
28
|
+
tag: str,
|
|
29
|
+
extra_opts: Optional[List[str]] = None,
|
|
30
|
+
):
|
|
31
|
+
build_configuration = _BuildConfig.parse_obj(build_configuration).__root__
|
|
32
|
+
if isinstance(build_configuration, TaskPythonBuild):
|
|
33
|
+
build_configuration_dict = build_configuration.dict()
|
|
34
|
+
build_configuration_dict.update({"type": "tfy-python-buildpack", "command": ""})
|
|
35
|
+
build_configuration = PythonBuild.parse_obj(build_configuration_dict)
|
|
36
|
+
if isinstance(build_configuration, TaskDockerFileBuild):
|
|
37
|
+
build_configuration_dict = build_configuration.dict()
|
|
38
|
+
build_configuration_dict.update({"type": "dockerfile"})
|
|
39
|
+
build_configuration = DockerFileBuild.parse_obj(build_configuration_dict)
|
|
40
|
+
|
|
41
|
+
builder = get_builder(build_configuration.type)
|
|
42
|
+
return builder(
|
|
43
|
+
build_configuration=build_configuration,
|
|
44
|
+
tag=tag,
|
|
45
|
+
extra_opts=extra_opts,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
# TODO: remove these and write tests for Build class to dockerfile content.
|
|
51
|
+
import os
|
|
52
|
+
from tempfile import TemporaryDirectory
|
|
53
|
+
|
|
54
|
+
with TemporaryDirectory() as local_dir:
|
|
55
|
+
dockerfile_path = os.path.join(local_dir, "Dockerfile.test")
|
|
56
|
+
with open(dockerfile_path, "w", encoding="utf8") as fp:
|
|
57
|
+
fp.write("from postgres:latest")
|
|
58
|
+
|
|
59
|
+
build_config = DockerFileBuild(
|
|
60
|
+
type="dockerfile",
|
|
61
|
+
build_context_path=local_dir,
|
|
62
|
+
dockerfile_path=dockerfile_path,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
build(tag="docker-test", build_configuration=build_config)
|
|
66
|
+
|
|
67
|
+
with TemporaryDirectory() as local_dir:
|
|
68
|
+
requirements_path = os.path.join(local_dir, "requirements.txt")
|
|
69
|
+
with open(requirements_path, "w", encoding="utf8") as fp:
|
|
70
|
+
fp.write("requests")
|
|
71
|
+
|
|
72
|
+
python_path = os.path.join(local_dir, "main.py")
|
|
73
|
+
with open(python_path, "w", encoding="utf8") as fp:
|
|
74
|
+
fp.write("import requests; print(requests.__version__);")
|
|
75
|
+
|
|
76
|
+
build_config = PythonBuild(
|
|
77
|
+
type="tfy-python-buildpack",
|
|
78
|
+
build_context_path=local_dir,
|
|
79
|
+
command=["python", "main.py"],
|
|
80
|
+
python_version="3.7.13",
|
|
81
|
+
)
|
|
82
|
+
build(tag="python-test-exec", build_configuration=build_config)
|
|
83
|
+
build_config = PythonBuild(
|
|
84
|
+
type="tfy-python-buildpack",
|
|
85
|
+
build_context_path=local_dir,
|
|
86
|
+
command="python main.py",
|
|
87
|
+
python_version="3.8",
|
|
88
|
+
)
|
|
89
|
+
build(tag="python-test-shell", build_configuration=build_config)
|
|
90
|
+
|
|
91
|
+
with TemporaryDirectory() as local_dir:
|
|
92
|
+
requirements_path = os.path.join(local_dir, "requirements.txt")
|
|
93
|
+
with open(requirements_path, "w", encoding="utf8") as fp:
|
|
94
|
+
fp.write("numpy")
|
|
95
|
+
|
|
96
|
+
python_path = os.path.join(local_dir, "main.py")
|
|
97
|
+
with open(python_path, "w", encoding="utf8") as fp:
|
|
98
|
+
fp.write("import numpy; import requests; print(requests.__version__);")
|
|
99
|
+
|
|
100
|
+
build_config = PythonBuild(
|
|
101
|
+
type="tfy-python-buildpack",
|
|
102
|
+
build_context_path=local_dir,
|
|
103
|
+
command=["python", "main.py"],
|
|
104
|
+
python_version="3.7.13",
|
|
105
|
+
pip_packages=["requests"],
|
|
106
|
+
)
|
|
107
|
+
build(tag="python-pip-and-req-file", build_configuration=build_config)
|
|
108
|
+
|
|
109
|
+
with TemporaryDirectory() as local_dir:
|
|
110
|
+
python_path = os.path.join(local_dir, "main.py")
|
|
111
|
+
with open(python_path, "w", encoding="utf8") as fp:
|
|
112
|
+
fp.write("import requests; print(requests.__version__);")
|
|
113
|
+
|
|
114
|
+
build_config = PythonBuild(
|
|
115
|
+
type="tfy-python-buildpack",
|
|
116
|
+
build_context_path=local_dir,
|
|
117
|
+
command=["python", "main.py"],
|
|
118
|
+
python_version="3.7.13",
|
|
119
|
+
pip_packages=["requests>0.1"],
|
|
120
|
+
)
|
|
121
|
+
build(tag="python-only-pip", build_configuration=build_config)
|
|
122
|
+
|
|
123
|
+
with TemporaryDirectory() as local_dir:
|
|
124
|
+
python_path = os.path.join(local_dir, "main.py")
|
|
125
|
+
with open(python_path, "w", encoding="utf8") as fp:
|
|
126
|
+
fp.write("print(1)")
|
|
127
|
+
|
|
128
|
+
build_config = PythonBuild(
|
|
129
|
+
type="tfy-python-buildpack",
|
|
130
|
+
build_context_path=local_dir,
|
|
131
|
+
command=["python", "main.py"],
|
|
132
|
+
python_version="3.7.13",
|
|
133
|
+
)
|
|
134
|
+
build(tag="python-only-no-pip-no-req", build_configuration=build_config)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Callable, Dict
|
|
2
|
+
|
|
3
|
+
from truefoundry.deploy.builder.builders import (
|
|
4
|
+
dockerfile,
|
|
5
|
+
tfy_notebook_buildpack,
|
|
6
|
+
tfy_python_buildpack,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
BUILD_REGISTRY: Dict[str, Callable] = {
|
|
10
|
+
"dockerfile": dockerfile.build,
|
|
11
|
+
"tfy-python-buildpack": tfy_python_buildpack.build,
|
|
12
|
+
"tfy-notebook-buildpack": tfy_notebook_buildpack.build,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
__all__ = ["get_builder"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_builder(build_configuration_type: str) -> Callable:
|
|
19
|
+
if build_configuration_type not in BUILD_REGISTRY:
|
|
20
|
+
raise NotImplementedError(f"Builder for {build_configuration_type} not found")
|
|
21
|
+
|
|
22
|
+
return BUILD_REGISTRY[build_configuration_type]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from truefoundry.deploy.auto_gen.models import DockerFileBuild
|
|
5
|
+
from truefoundry.deploy.builder.docker_service import build_docker_image
|
|
6
|
+
from truefoundry.logger import logger
|
|
7
|
+
|
|
8
|
+
__all__ = ["build"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_expanded_and_absolute_path(path: str):
|
|
12
|
+
return os.path.abspath(os.path.expanduser(path))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _build_docker_image(
|
|
16
|
+
tag: str,
|
|
17
|
+
path: str = ".",
|
|
18
|
+
file: Optional[str] = None,
|
|
19
|
+
build_args: Optional[Dict[str, str]] = None,
|
|
20
|
+
extra_opts: Optional[List[str]] = None,
|
|
21
|
+
):
|
|
22
|
+
path = _get_expanded_and_absolute_path(path)
|
|
23
|
+
|
|
24
|
+
if file:
|
|
25
|
+
file = _get_expanded_and_absolute_path(file)
|
|
26
|
+
|
|
27
|
+
build_docker_image(
|
|
28
|
+
path=path,
|
|
29
|
+
tag=tag,
|
|
30
|
+
# TODO: can we pick target platform(s) picked from cluster
|
|
31
|
+
platform="linux/amd64",
|
|
32
|
+
dockerfile=file,
|
|
33
|
+
build_args=build_args,
|
|
34
|
+
extra_opts=extra_opts,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build(
|
|
39
|
+
tag: str,
|
|
40
|
+
build_configuration: DockerFileBuild,
|
|
41
|
+
extra_opts: Optional[List[str]] = None,
|
|
42
|
+
):
|
|
43
|
+
dockerfile_path = _get_expanded_and_absolute_path(
|
|
44
|
+
build_configuration.dockerfile_path
|
|
45
|
+
)
|
|
46
|
+
with open(dockerfile_path) as f:
|
|
47
|
+
dockerfile_content = f.read()
|
|
48
|
+
logger.info("Dockerfile content:-")
|
|
49
|
+
logger.info(dockerfile_content)
|
|
50
|
+
|
|
51
|
+
_build_docker_image(
|
|
52
|
+
tag=tag,
|
|
53
|
+
path=build_configuration.build_context_path,
|
|
54
|
+
file=build_configuration.dockerfile_path,
|
|
55
|
+
build_args=build_configuration.build_args,
|
|
56
|
+
extra_opts=extra_opts,
|
|
57
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from tempfile import TemporaryDirectory
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from truefoundry.deploy.auto_gen.models import DockerFileBuild
|
|
6
|
+
from truefoundry.deploy.builder.builders import dockerfile
|
|
7
|
+
from truefoundry.deploy.builder.builders.tfy_notebook_buildpack.dockerfile_template import (
|
|
8
|
+
NotebookImageBuild,
|
|
9
|
+
generate_dockerfile_content,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = ["generate_dockerfile_content", "build"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _convert_to_dockerfile_build_config(
|
|
16
|
+
build_configuration: NotebookImageBuild, local_dir: str
|
|
17
|
+
) -> DockerFileBuild:
|
|
18
|
+
dockerfile_content = generate_dockerfile_content(
|
|
19
|
+
build_configuration=build_configuration,
|
|
20
|
+
local_dir=local_dir,
|
|
21
|
+
)
|
|
22
|
+
dockerfile_path = os.path.join(local_dir, "Dockerfile")
|
|
23
|
+
with open(dockerfile_path, "w", encoding="utf8") as fp:
|
|
24
|
+
fp.write(dockerfile_content)
|
|
25
|
+
return DockerFileBuild(
|
|
26
|
+
type="dockerfile",
|
|
27
|
+
dockerfile_path=dockerfile_path,
|
|
28
|
+
build_context_path=local_dir,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build(
|
|
33
|
+
tag: str,
|
|
34
|
+
build_configuration: NotebookImageBuild,
|
|
35
|
+
extra_opts: Optional[List[str]] = None,
|
|
36
|
+
):
|
|
37
|
+
with TemporaryDirectory() as local_dir:
|
|
38
|
+
docker_build_configuration = _convert_to_dockerfile_build_config(
|
|
39
|
+
build_configuration,
|
|
40
|
+
local_dir=local_dir,
|
|
41
|
+
)
|
|
42
|
+
dockerfile.build(
|
|
43
|
+
tag=tag,
|
|
44
|
+
build_configuration=docker_build_configuration,
|
|
45
|
+
extra_opts=extra_opts,
|
|
46
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
from typing import Literal, Optional
|
|
4
|
+
|
|
5
|
+
from mako.template import Template
|
|
6
|
+
|
|
7
|
+
from truefoundry.pydantic_v1 import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NotebookImageBuild(BaseModel):
|
|
11
|
+
type: Literal["tfy-notebook-buildpack"] = "tfy-notebook-buildpack"
|
|
12
|
+
base_image_uri: str
|
|
13
|
+
build_script: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DOCKERFILE_TEMPLATE = Template(
|
|
17
|
+
"""
|
|
18
|
+
FROM ${base_image_uri}
|
|
19
|
+
USER root
|
|
20
|
+
|
|
21
|
+
% if build_script_docker_commands is not None:
|
|
22
|
+
${build_script_docker_commands}
|
|
23
|
+
% endif
|
|
24
|
+
|
|
25
|
+
USER $NB_UID
|
|
26
|
+
"""
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def generate_build_script_docker_commands(
|
|
31
|
+
build_script: Optional[str], local_dir: str
|
|
32
|
+
) -> Optional[str]:
|
|
33
|
+
if not build_script:
|
|
34
|
+
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)
|
|
43
|
+
run_build_script_command = f"""\
|
|
44
|
+
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
|
|
46
|
+
"""
|
|
47
|
+
return run_build_script_command
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def generate_dockerfile_content(
|
|
51
|
+
build_configuration: NotebookImageBuild, local_dir: str
|
|
52
|
+
) -> str:
|
|
53
|
+
build_script_docker_commands = generate_build_script_docker_commands(
|
|
54
|
+
build_script=build_configuration.build_script,
|
|
55
|
+
local_dir=local_dir,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
template_args = {
|
|
59
|
+
"base_image_uri": build_configuration.base_image_uri,
|
|
60
|
+
"build_script_docker_commands": build_script_docker_commands,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
template = DOCKERFILE_TEMPLATE
|
|
64
|
+
|
|
65
|
+
dockerfile_content = template.render(**template_args)
|
|
66
|
+
return dockerfile_content
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from tempfile import TemporaryDirectory
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from truefoundry.deploy.auto_gen.models import DockerFileBuild, PythonBuild
|
|
6
|
+
from truefoundry.deploy.builder.builders import dockerfile
|
|
7
|
+
from truefoundry.deploy.builder.builders.tfy_python_buildpack.dockerfile_template import (
|
|
8
|
+
generate_dockerfile_content,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = ["generate_dockerfile_content", "build"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _convert_to_dockerfile_build_config(
|
|
15
|
+
build_configuration: PythonBuild,
|
|
16
|
+
dockerfile_path: str,
|
|
17
|
+
) -> DockerFileBuild:
|
|
18
|
+
dockerfile_content = generate_dockerfile_content(
|
|
19
|
+
build_configuration=build_configuration
|
|
20
|
+
)
|
|
21
|
+
with open(dockerfile_path, "w", encoding="utf8") as fp:
|
|
22
|
+
fp.write(dockerfile_content)
|
|
23
|
+
|
|
24
|
+
return DockerFileBuild(
|
|
25
|
+
type="dockerfile",
|
|
26
|
+
dockerfile_path=dockerfile_path,
|
|
27
|
+
build_context_path=build_configuration.build_context_path,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build(
|
|
32
|
+
tag: str,
|
|
33
|
+
build_configuration: PythonBuild,
|
|
34
|
+
extra_opts: Optional[List[str]] = None,
|
|
35
|
+
):
|
|
36
|
+
with TemporaryDirectory() as local_dir:
|
|
37
|
+
docker_build_configuration = _convert_to_dockerfile_build_config(
|
|
38
|
+
build_configuration, dockerfile_path=os.path.join(local_dir, "Dockerfile")
|
|
39
|
+
)
|
|
40
|
+
dockerfile.build(
|
|
41
|
+
tag=tag,
|
|
42
|
+
build_configuration=docker_build_configuration,
|
|
43
|
+
extra_opts=extra_opts,
|
|
44
|
+
)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from mako.template import Template
|
|
5
|
+
|
|
6
|
+
from truefoundry.deploy.auto_gen.models import PythonBuild
|
|
7
|
+
from truefoundry.deploy.v2.lib.patched_models import CUDAVersion
|
|
8
|
+
|
|
9
|
+
# TODO (chiragjn): Switch to a non-root user inside the container
|
|
10
|
+
|
|
11
|
+
_POST_PYTHON_INSTALL_TEMPLATE = """
|
|
12
|
+
% if apt_install_command is not None:
|
|
13
|
+
RUN ${apt_install_command}
|
|
14
|
+
% endif
|
|
15
|
+
|
|
16
|
+
% if requirements_path is not None:
|
|
17
|
+
COPY ${requirements_path} ${requirements_destination_path}
|
|
18
|
+
% endif
|
|
19
|
+
|
|
20
|
+
% if pip_install_command is not None:
|
|
21
|
+
RUN ${pip_install_command}
|
|
22
|
+
% endif
|
|
23
|
+
|
|
24
|
+
COPY . /app
|
|
25
|
+
WORKDIR /app
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
DOCKERFILE_TEMPLATE = Template(
|
|
29
|
+
"""
|
|
30
|
+
FROM --platform=linux/amd64 python:${python_version}
|
|
31
|
+
ENV PATH=/virtualenvs/venv/bin:$PATH
|
|
32
|
+
RUN apt update && \
|
|
33
|
+
DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends git && \
|
|
34
|
+
python -m venv /virtualenvs/venv/ && \
|
|
35
|
+
rm -rf /var/lib/apt/lists/*
|
|
36
|
+
"""
|
|
37
|
+
+ _POST_PYTHON_INSTALL_TEMPLATE
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
CUDA_DOCKERFILE_TEMPLATE = Template(
|
|
41
|
+
"""
|
|
42
|
+
FROM --platform=linux/amd64 nvidia/cuda:${nvidia_cuda_image_tag}
|
|
43
|
+
ENV PATH=/virtualenvs/venv/bin:$PATH
|
|
44
|
+
RUN echo "deb https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu $(cat /etc/os-release | grep UBUNTU_CODENAME | cut -d = -f 2) main" >> /etc/apt/sources.list && \
|
|
45
|
+
echo "deb-src https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu $(cat /etc/os-release | grep UBUNTU_CODENAME | cut -d = -f 2) main" >> /etc/apt/sources.list && \
|
|
46
|
+
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F23C5A6CF475977595C89F51BA6932366A755776 && \
|
|
47
|
+
apt update && \
|
|
48
|
+
DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends git python${python_version}-dev python${python_version}-venv && \
|
|
49
|
+
python${python_version} -m venv /virtualenvs/venv/ && \
|
|
50
|
+
rm -rf /var/lib/apt/lists/*
|
|
51
|
+
"""
|
|
52
|
+
+ _POST_PYTHON_INSTALL_TEMPLATE
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
CUDA_VERSION_TO_IMAGE_TAG: Dict[str, str] = {
|
|
56
|
+
CUDAVersion.CUDA_11_0_CUDNN8.value: "11.0.3-cudnn8-runtime-ubuntu20.04",
|
|
57
|
+
CUDAVersion.CUDA_11_1_CUDNN8.value: "11.1.1-cudnn8-runtime-ubuntu20.04",
|
|
58
|
+
CUDAVersion.CUDA_11_2_CUDNN8.value: "11.2.2-cudnn8-runtime-ubuntu20.04",
|
|
59
|
+
CUDAVersion.CUDA_11_3_CUDNN8.value: "11.3.1-cudnn8-runtime-ubuntu20.04",
|
|
60
|
+
CUDAVersion.CUDA_11_4_CUDNN8.value: "11.4.3-cudnn8-runtime-ubuntu20.04",
|
|
61
|
+
CUDAVersion.CUDA_11_5_CUDNN8.value: "11.5.2-cudnn8-runtime-ubuntu20.04",
|
|
62
|
+
CUDAVersion.CUDA_11_6_CUDNN8.value: "11.6.2-cudnn8-runtime-ubuntu20.04",
|
|
63
|
+
CUDAVersion.CUDA_11_7_CUDNN8.value: "11.7.1-cudnn8-runtime-ubuntu22.04",
|
|
64
|
+
CUDAVersion.CUDA_11_8_CUDNN8.value: "11.8.0-cudnn8-runtime-ubuntu22.04",
|
|
65
|
+
CUDAVersion.CUDA_12_0_CUDNN8.value: "12.0.1-cudnn8-runtime-ubuntu22.04",
|
|
66
|
+
CUDAVersion.CUDA_12_1_CUDNN8.value: "12.1.1-cudnn8-runtime-ubuntu22.04",
|
|
67
|
+
CUDAVersion.CUDA_12_2_CUDNN8.value: "12.2.2-cudnn8-runtime-ubuntu22.04",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve_requirements_txt_path(build_configuration: PythonBuild) -> Optional[str]:
|
|
72
|
+
if build_configuration.requirements_path:
|
|
73
|
+
return build_configuration.requirements_path
|
|
74
|
+
|
|
75
|
+
# TODO: what if there is a requirements.txt but user does not wants us to use it.
|
|
76
|
+
possible_requirements_txt_path = os.path.join(
|
|
77
|
+
build_configuration.build_context_path, "requirements.txt"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if os.path.isfile(possible_requirements_txt_path):
|
|
81
|
+
return os.path.relpath(
|
|
82
|
+
possible_requirements_txt_path, start=build_configuration.build_context_path
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def generate_apt_install_command(apt_packages: Optional[List[str]]) -> Optional[str]:
|
|
89
|
+
packages_list = None
|
|
90
|
+
if apt_packages:
|
|
91
|
+
packages_list = " ".join(p.strip() for p in apt_packages if p.strip())
|
|
92
|
+
if not packages_list:
|
|
93
|
+
return None
|
|
94
|
+
apt_update_command = "apt update"
|
|
95
|
+
apt_install_command = f"DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends {packages_list}"
|
|
96
|
+
clear_apt_lists_command = "rm -rf /var/lib/apt/lists/*"
|
|
97
|
+
return " && ".join(
|
|
98
|
+
[apt_update_command, apt_install_command, clear_apt_lists_command]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def generate_pip_install_command(
|
|
103
|
+
requirements_path: Optional[str], pip_packages: Optional[List[str]]
|
|
104
|
+
) -> Optional[str]:
|
|
105
|
+
upgrade_pip_command = "python -m pip install -U pip setuptools wheel"
|
|
106
|
+
final_pip_install_command = None
|
|
107
|
+
pip_install_base_command = "python -m pip install --use-pep517 --no-cache-dir"
|
|
108
|
+
if requirements_path:
|
|
109
|
+
final_pip_install_command = f"{pip_install_base_command} -r {requirements_path}"
|
|
110
|
+
|
|
111
|
+
if pip_packages:
|
|
112
|
+
final_pip_install_command = (
|
|
113
|
+
final_pip_install_command or pip_install_base_command
|
|
114
|
+
)
|
|
115
|
+
final_pip_install_command += " " + " ".join(
|
|
116
|
+
f"'{package}'" for package in pip_packages
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if not final_pip_install_command:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
return " && ".join([upgrade_pip_command, final_pip_install_command])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def generate_dockerfile_content(
|
|
126
|
+
build_configuration: PythonBuild,
|
|
127
|
+
) -> str:
|
|
128
|
+
# TODO (chiragjn): Handle recursive references to other requirements files e.g. `-r requirements-gpu.txt`
|
|
129
|
+
requirements_path = resolve_requirements_txt_path(build_configuration)
|
|
130
|
+
requirements_destination_path = (
|
|
131
|
+
"/tmp/requirements.txt" if requirements_path else None
|
|
132
|
+
)
|
|
133
|
+
pip_install_command = generate_pip_install_command(
|
|
134
|
+
requirements_path=requirements_destination_path,
|
|
135
|
+
pip_packages=build_configuration.pip_packages,
|
|
136
|
+
)
|
|
137
|
+
apt_install_command = generate_apt_install_command(
|
|
138
|
+
apt_packages=build_configuration.apt_packages
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
template_args = {
|
|
142
|
+
"python_version": build_configuration.python_version,
|
|
143
|
+
"apt_install_command": apt_install_command,
|
|
144
|
+
"requirements_path": requirements_path,
|
|
145
|
+
"requirements_destination_path": requirements_destination_path,
|
|
146
|
+
"pip_install_command": pip_install_command,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if build_configuration.cuda_version:
|
|
150
|
+
template = CUDA_DOCKERFILE_TEMPLATE
|
|
151
|
+
template_args["nvidia_cuda_image_tag"] = CUDA_VERSION_TO_IMAGE_TAG.get(
|
|
152
|
+
build_configuration.cuda_version, build_configuration.cuda_version
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
template = DOCKERFILE_TEMPLATE
|
|
156
|
+
|
|
157
|
+
dockerfile_content = template.render(**template_args)
|
|
158
|
+
return dockerfile_content
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
import docker
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from truefoundry.deploy.lib.clients.shell_client import Shell
|
|
9
|
+
from truefoundry.logger import logger
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"build_docker_image",
|
|
13
|
+
"push_docker_image",
|
|
14
|
+
"pull_docker_image",
|
|
15
|
+
"push_docker_image_with_latest_tag",
|
|
16
|
+
"env_has_docker",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_build_args_string(build_args: Optional[Dict[str, str]] = None) -> str:
|
|
21
|
+
if not build_args:
|
|
22
|
+
return None
|
|
23
|
+
result = []
|
|
24
|
+
for param, value in build_args.items():
|
|
25
|
+
result.extend(["--build-arg", f"{param.strip()}={value}"])
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_docker_client():
|
|
30
|
+
try:
|
|
31
|
+
return docker.from_env()
|
|
32
|
+
except Exception as ex:
|
|
33
|
+
raise Exception("Could not connect to Docker") from ex
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def env_has_docker():
|
|
37
|
+
try:
|
|
38
|
+
_get_docker_client()
|
|
39
|
+
return True
|
|
40
|
+
except Exception:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# this is required since push does throw an error if it
|
|
45
|
+
# fails - so we have to parse the response logs to catch the error
|
|
46
|
+
# the other option is to run `docker login` as a subprocess, but it's
|
|
47
|
+
# not recommended to provide password to subprocess
|
|
48
|
+
def _catch_error_in_push(response: List[dict]):
|
|
49
|
+
for line in response:
|
|
50
|
+
if line.get("error") is not None:
|
|
51
|
+
raise Exception(
|
|
52
|
+
f'Failed to push to registry with message \'{line.get("error")}\''
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _run_cmds(command: List[str]):
|
|
57
|
+
console = Console(color_system=None, markup=False, soft_wrap=True)
|
|
58
|
+
with subprocess.Popen(
|
|
59
|
+
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
|
60
|
+
) as sp:
|
|
61
|
+
for line in sp.stdout:
|
|
62
|
+
console.print(line.decode("utf-8").strip())
|
|
63
|
+
sp.communicate()
|
|
64
|
+
if sp.returncode is not None and sp.returncode != 0:
|
|
65
|
+
raise Exception(f"Command: {command} failed")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def build_docker_image(
|
|
69
|
+
path: str,
|
|
70
|
+
tag: str,
|
|
71
|
+
platform: str,
|
|
72
|
+
dockerfile: str,
|
|
73
|
+
extra_opts: Optional[List[str]] = None,
|
|
74
|
+
build_args: Optional[Dict[str, str]] = None,
|
|
75
|
+
):
|
|
76
|
+
use_depot = bool(os.environ.get("USE_DEPOT"))
|
|
77
|
+
depot_project_id = os.environ.get("DEPOT_PROJECT_KEY")
|
|
78
|
+
logger.info("Starting docker build...")
|
|
79
|
+
if use_depot and depot_project_id:
|
|
80
|
+
try:
|
|
81
|
+
command = [
|
|
82
|
+
"depot",
|
|
83
|
+
"build",
|
|
84
|
+
"--project",
|
|
85
|
+
depot_project_id,
|
|
86
|
+
"-f",
|
|
87
|
+
dockerfile,
|
|
88
|
+
"-t",
|
|
89
|
+
tag,
|
|
90
|
+
path,
|
|
91
|
+
]
|
|
92
|
+
final_build_args = _get_build_args_string(build_args=build_args)
|
|
93
|
+
if final_build_args:
|
|
94
|
+
command.extend(final_build_args)
|
|
95
|
+
command.append("--push") # keep push at last
|
|
96
|
+
Shell().execute_shell_command(command=command)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise Exception("Error while building Docker image using Depot") from e
|
|
99
|
+
else:
|
|
100
|
+
try:
|
|
101
|
+
# TODO (chiragjn): Maybe consider using client.images.build
|
|
102
|
+
build_args_list = []
|
|
103
|
+
if build_args:
|
|
104
|
+
for k, v in build_args.items():
|
|
105
|
+
build_args_list += ["--build-arg", f"{k}={v}"]
|
|
106
|
+
|
|
107
|
+
docker_build_cmd = [
|
|
108
|
+
"docker",
|
|
109
|
+
"build",
|
|
110
|
+
"-t",
|
|
111
|
+
tag,
|
|
112
|
+
"-f",
|
|
113
|
+
dockerfile,
|
|
114
|
+
"--platform",
|
|
115
|
+
platform,
|
|
116
|
+
]
|
|
117
|
+
docker_build_cmd += [path]
|
|
118
|
+
docker_build_cmd += build_args_list
|
|
119
|
+
docker_build_cmd += extra_opts if extra_opts else []
|
|
120
|
+
_run_cmds(docker_build_cmd)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
raise Exception(f"Error while building Docker image: {e}") from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def push_docker_image(
|
|
126
|
+
image_uri: str,
|
|
127
|
+
docker_login_username: str,
|
|
128
|
+
docker_login_password: str,
|
|
129
|
+
):
|
|
130
|
+
client = _get_docker_client()
|
|
131
|
+
auth_config = {"username": docker_login_username, "password": docker_login_password}
|
|
132
|
+
logger.info(f"Pushing {image_uri}")
|
|
133
|
+
response = client.images.push(
|
|
134
|
+
repository=image_uri, auth_config=auth_config, decode=True, stream=True
|
|
135
|
+
)
|
|
136
|
+
_catch_error_in_push(response=response)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def push_docker_image_with_latest_tag(
|
|
140
|
+
image_uri: str,
|
|
141
|
+
docker_login_username: str,
|
|
142
|
+
docker_login_password: str,
|
|
143
|
+
):
|
|
144
|
+
client = _get_docker_client()
|
|
145
|
+
auth_config = {"username": docker_login_username, "password": docker_login_password}
|
|
146
|
+
repository_without_tag, _ = image_uri.rsplit(":", 1)
|
|
147
|
+
image = client.images.get(image_uri)
|
|
148
|
+
image.tag(repository=repository_without_tag, tag="latest")
|
|
149
|
+
|
|
150
|
+
logger.info(f"Pushing {repository_without_tag}:latest")
|
|
151
|
+
response = client.images.push(
|
|
152
|
+
repository=repository_without_tag,
|
|
153
|
+
tag="latest",
|
|
154
|
+
auth_config=auth_config,
|
|
155
|
+
decode=True,
|
|
156
|
+
stream=True,
|
|
157
|
+
)
|
|
158
|
+
_catch_error_in_push(response=response)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def pull_docker_image(
|
|
162
|
+
image_uri: str,
|
|
163
|
+
docker_login_username: str,
|
|
164
|
+
docker_login_password: str,
|
|
165
|
+
):
|
|
166
|
+
auth_config = {"username": docker_login_username, "password": docker_login_password}
|
|
167
|
+
logger.info(f"Pulling cache image {image_uri}")
|
|
168
|
+
_get_docker_client().images.pull(repository=image_uri, auth_config=auth_config)
|