flowyml 1.7.1__py3-none-any.whl → 1.8.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.
- flowyml/assets/base.py +15 -0
- flowyml/assets/dataset.py +570 -17
- flowyml/assets/metrics.py +5 -0
- flowyml/assets/model.py +1052 -15
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +231 -37
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +59 -4
- flowyml/core/pipeline.py +65 -13
- flowyml/core/routing.py +558 -0
- flowyml/core/scheduler.py +88 -5
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- flowyml/integrations/keras.py +247 -82
- flowyml/monitoring/alerts.py +10 -0
- flowyml/monitoring/notifications.py +104 -25
- flowyml/monitoring/slack_blocks.py +323 -0
- flowyml/plugins/__init__.py +251 -0
- flowyml/plugins/alerters/__init__.py +1 -0
- flowyml/plugins/alerters/slack.py +168 -0
- flowyml/plugins/base.py +752 -0
- flowyml/plugins/config.py +478 -0
- flowyml/plugins/deployers/__init__.py +22 -0
- flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
- flowyml/plugins/deployers/sagemaker.py +306 -0
- flowyml/plugins/deployers/vertex.py +290 -0
- flowyml/plugins/integration.py +369 -0
- flowyml/plugins/manager.py +510 -0
- flowyml/plugins/model_registries/__init__.py +22 -0
- flowyml/plugins/model_registries/mlflow.py +159 -0
- flowyml/plugins/model_registries/sagemaker.py +489 -0
- flowyml/plugins/model_registries/vertex.py +386 -0
- flowyml/plugins/orchestrators/__init__.py +13 -0
- flowyml/plugins/orchestrators/sagemaker.py +443 -0
- flowyml/plugins/orchestrators/vertex_ai.py +461 -0
- flowyml/plugins/registries/__init__.py +13 -0
- flowyml/plugins/registries/ecr.py +321 -0
- flowyml/plugins/registries/gcr.py +313 -0
- flowyml/plugins/registry.py +454 -0
- flowyml/plugins/stack.py +494 -0
- flowyml/plugins/stack_config.py +537 -0
- flowyml/plugins/stores/__init__.py +13 -0
- flowyml/plugins/stores/gcs.py +460 -0
- flowyml/plugins/stores/s3.py +453 -0
- flowyml/plugins/trackers/__init__.py +11 -0
- flowyml/plugins/trackers/mlflow.py +316 -0
- flowyml/plugins/validators/__init__.py +3 -0
- flowyml/plugins/validators/deepchecks.py +119 -0
- flowyml/registry/__init__.py +2 -1
- flowyml/registry/model_environment.py +109 -0
- flowyml/registry/model_registry.py +241 -96
- flowyml/serving/__init__.py +17 -0
- flowyml/serving/model_server.py +628 -0
- flowyml/stacks/__init__.py +60 -0
- flowyml/stacks/aws.py +93 -0
- flowyml/stacks/base.py +62 -0
- flowyml/stacks/components.py +12 -0
- flowyml/stacks/gcp.py +44 -9
- flowyml/stacks/plugins.py +115 -0
- flowyml/stacks/registry.py +2 -1
- flowyml/storage/sql.py +401 -12
- flowyml/tracking/experiment.py +8 -5
- flowyml/ui/backend/Dockerfile +87 -16
- flowyml/ui/backend/auth.py +12 -2
- flowyml/ui/backend/main.py +149 -5
- flowyml/ui/backend/routers/ai_context.py +226 -0
- flowyml/ui/backend/routers/assets.py +23 -4
- flowyml/ui/backend/routers/auth.py +96 -0
- flowyml/ui/backend/routers/deployments.py +660 -0
- flowyml/ui/backend/routers/model_explorer.py +597 -0
- flowyml/ui/backend/routers/plugins.py +103 -51
- flowyml/ui/backend/routers/projects.py +91 -8
- flowyml/ui/backend/routers/runs.py +132 -1
- flowyml/ui/backend/routers/schedules.py +54 -29
- flowyml/ui/backend/routers/templates.py +319 -0
- flowyml/ui/backend/routers/websocket.py +2 -2
- flowyml/ui/frontend/Dockerfile +55 -6
- flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
- flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/dist/logo.png +0 -0
- flowyml/ui/frontend/nginx.conf +65 -4
- flowyml/ui/frontend/package-lock.json +1415 -74
- flowyml/ui/frontend/package.json +4 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
- flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
- flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
- flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
- flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
- flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
- flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
- flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
- flowyml/ui/frontend/src/components/Layout.jsx +6 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
- flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
- flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
- flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
- flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
- flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
- flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
- flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
- flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
- flowyml/ui/frontend/src/router/index.jsx +47 -20
- flowyml/ui/frontend/src/services/pluginService.js +3 -1
- flowyml/ui/server_manager.py +5 -5
- flowyml/ui/utils.py +157 -39
- flowyml/utils/config.py +37 -15
- flowyml/utils/model_introspection.py +123 -0
- flowyml/utils/observability.py +30 -0
- flowyml-1.8.0.dist-info/METADATA +174 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
- flowyml-1.7.1.dist-info/METADATA +0 -477
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""AWS ECR Container Registry - Native FlowyML Plugin.
|
|
2
|
+
|
|
3
|
+
This is a native FlowyML implementation for AWS Elastic Container Registry,
|
|
4
|
+
without requiring any external framework dependencies.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from flowyml.plugins import get_plugin
|
|
8
|
+
|
|
9
|
+
registry = get_plugin("ecr",
|
|
10
|
+
repository="my-ml-images",
|
|
11
|
+
region="us-east-1"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Push an image
|
|
15
|
+
uri = registry.push_image("ml-training", tag="v1.0")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import subprocess
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
from flowyml.plugins.base import ContainerRegistryPlugin, PluginMetadata, PluginType
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ECRRegistry(ContainerRegistryPlugin):
|
|
27
|
+
"""Native AWS ECR container registry for FlowyML.
|
|
28
|
+
|
|
29
|
+
This registry integrates directly with AWS ECR without any
|
|
30
|
+
intermediate framework.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
repository: ECR repository name.
|
|
34
|
+
region: AWS region.
|
|
35
|
+
account_id: AWS account ID (auto-detected if not provided).
|
|
36
|
+
access_key: AWS access key (uses environment/credentials if not provided).
|
|
37
|
+
secret_key: AWS secret key (uses environment/credentials if not provided).
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
registry = ECRRegistry(
|
|
41
|
+
repository="ml-images",
|
|
42
|
+
region="us-east-1"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
uri = registry.push_image("classifier", tag="v1.0")
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
METADATA = PluginMetadata(
|
|
49
|
+
name="ecr",
|
|
50
|
+
description="AWS Elastic Container Registry",
|
|
51
|
+
plugin_type=PluginType.CONTAINER_REGISTRY,
|
|
52
|
+
version="1.0.0",
|
|
53
|
+
author="FlowyML",
|
|
54
|
+
packages=["boto3>=1.28"],
|
|
55
|
+
documentation_url="https://docs.aws.amazon.com/ecr/",
|
|
56
|
+
tags=["container-registry", "aws", "cloud"],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
repository: str,
|
|
62
|
+
region: str = None,
|
|
63
|
+
account_id: str = None,
|
|
64
|
+
access_key: str = None,
|
|
65
|
+
secret_key: str = None,
|
|
66
|
+
**kwargs,
|
|
67
|
+
):
|
|
68
|
+
"""Initialize the ECR registry."""
|
|
69
|
+
super().__init__(
|
|
70
|
+
name=kwargs.pop("name", "ecr"),
|
|
71
|
+
repository=repository,
|
|
72
|
+
region=region,
|
|
73
|
+
account_id=account_id,
|
|
74
|
+
access_key=access_key,
|
|
75
|
+
secret_key=secret_key,
|
|
76
|
+
**kwargs,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self._repository = repository
|
|
80
|
+
self._region = region
|
|
81
|
+
self._account_id = account_id
|
|
82
|
+
self._ecr_client = None
|
|
83
|
+
|
|
84
|
+
def initialize(self) -> None:
|
|
85
|
+
"""Initialize ECR connection."""
|
|
86
|
+
try:
|
|
87
|
+
import boto3
|
|
88
|
+
|
|
89
|
+
# Build client kwargs
|
|
90
|
+
client_kwargs = {}
|
|
91
|
+
|
|
92
|
+
if self._region:
|
|
93
|
+
client_kwargs["region_name"] = self._region
|
|
94
|
+
|
|
95
|
+
if self._config.get("access_key") and self._config.get("secret_key"):
|
|
96
|
+
client_kwargs["aws_access_key_id"] = self._config["access_key"]
|
|
97
|
+
client_kwargs["aws_secret_access_key"] = self._config["secret_key"]
|
|
98
|
+
|
|
99
|
+
self._ecr_client = boto3.client("ecr", **client_kwargs)
|
|
100
|
+
|
|
101
|
+
# Auto-detect account ID if not provided
|
|
102
|
+
if not self._account_id:
|
|
103
|
+
sts = boto3.client("sts", **client_kwargs)
|
|
104
|
+
self._account_id = sts.get_caller_identity()["Account"]
|
|
105
|
+
|
|
106
|
+
# Get Docker login credentials
|
|
107
|
+
self._authenticate_docker()
|
|
108
|
+
|
|
109
|
+
self._is_initialized = True
|
|
110
|
+
logger.info(f"ECR registry initialized: {self.registry_uri}")
|
|
111
|
+
|
|
112
|
+
except ImportError:
|
|
113
|
+
raise ImportError(
|
|
114
|
+
"boto3 is not installed. Run: flowyml plugin install ecr",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def _authenticate_docker(self) -> None:
|
|
118
|
+
"""Authenticate Docker with ECR."""
|
|
119
|
+
try:
|
|
120
|
+
# Get authorization token
|
|
121
|
+
response = self._ecr_client.get_authorization_token()
|
|
122
|
+
auth_data = response["authorizationData"][0]
|
|
123
|
+
|
|
124
|
+
# Decode credentials
|
|
125
|
+
import base64
|
|
126
|
+
|
|
127
|
+
token = base64.b64decode(auth_data["authorizationToken"]).decode()
|
|
128
|
+
username, password = token.split(":")
|
|
129
|
+
registry_url = auth_data["proxyEndpoint"]
|
|
130
|
+
|
|
131
|
+
# Login to Docker
|
|
132
|
+
result = subprocess.run(
|
|
133
|
+
["docker", "login", "--username", username, "--password-stdin", registry_url],
|
|
134
|
+
input=password.encode(),
|
|
135
|
+
capture_output=True,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if result.returncode != 0:
|
|
139
|
+
logger.warning(f"Docker login may have failed: {result.stderr.decode()}")
|
|
140
|
+
else:
|
|
141
|
+
logger.debug("Docker authenticated with ECR")
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.warning(f"Failed to authenticate Docker with ECR: {e}")
|
|
145
|
+
|
|
146
|
+
def _ensure_initialized(self) -> None:
|
|
147
|
+
"""Ensure the registry is initialized."""
|
|
148
|
+
if not self._is_initialized:
|
|
149
|
+
self.initialize()
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def registry_uri(self) -> str:
|
|
153
|
+
"""Get the base registry URI."""
|
|
154
|
+
return f"{self._account_id}.dkr.ecr.{self._region}.amazonaws.com/{self._repository}"
|
|
155
|
+
|
|
156
|
+
def get_image_uri(self, image_name: str, tag: str = "latest") -> str:
|
|
157
|
+
"""Get the full URI for an image.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
image_name: Name of the image.
|
|
161
|
+
tag: Image tag.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Full image URI.
|
|
165
|
+
"""
|
|
166
|
+
return f"{self._account_id}.dkr.ecr.{self._region}.amazonaws.com/{self._repository}/{image_name}:{tag}"
|
|
167
|
+
|
|
168
|
+
def push_image(self, image_name: str, tag: str = "latest", local_image: str = None) -> str:
|
|
169
|
+
"""Push an image to ECR.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
image_name: Name for the image in the registry.
|
|
173
|
+
tag: Image tag.
|
|
174
|
+
local_image: Local image name to push.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Full image URI.
|
|
178
|
+
"""
|
|
179
|
+
self._ensure_initialized()
|
|
180
|
+
|
|
181
|
+
remote_uri = self.get_image_uri(image_name, tag)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
import docker
|
|
185
|
+
|
|
186
|
+
client = docker.from_env()
|
|
187
|
+
|
|
188
|
+
if local_image:
|
|
189
|
+
# Tag the local image with the remote URI
|
|
190
|
+
image = client.images.get(local_image)
|
|
191
|
+
image.tag(remote_uri)
|
|
192
|
+
|
|
193
|
+
# Push the image
|
|
194
|
+
logger.info(f"Pushing image to {remote_uri}...")
|
|
195
|
+
|
|
196
|
+
for line in client.images.push(remote_uri, stream=True, decode=True):
|
|
197
|
+
if "status" in line:
|
|
198
|
+
logger.debug(line["status"])
|
|
199
|
+
if "error" in line:
|
|
200
|
+
raise RuntimeError(line["error"])
|
|
201
|
+
|
|
202
|
+
logger.info(f"Successfully pushed {remote_uri}")
|
|
203
|
+
return remote_uri
|
|
204
|
+
|
|
205
|
+
except ImportError:
|
|
206
|
+
# Fall back to Docker CLI
|
|
207
|
+
logger.info("docker-py not available, using Docker CLI")
|
|
208
|
+
|
|
209
|
+
if local_image:
|
|
210
|
+
subprocess.run(
|
|
211
|
+
["docker", "tag", local_image, remote_uri],
|
|
212
|
+
check=True,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
result = subprocess.run(
|
|
216
|
+
["docker", "push", remote_uri],
|
|
217
|
+
capture_output=True,
|
|
218
|
+
text=True,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if result.returncode != 0:
|
|
222
|
+
raise RuntimeError(f"Failed to push image: {result.stderr}")
|
|
223
|
+
|
|
224
|
+
logger.info(f"Successfully pushed {remote_uri}")
|
|
225
|
+
return remote_uri
|
|
226
|
+
|
|
227
|
+
def pull_image(self, image_name: str, tag: str = "latest") -> None:
|
|
228
|
+
"""Pull an image from ECR."""
|
|
229
|
+
self._ensure_initialized()
|
|
230
|
+
|
|
231
|
+
remote_uri = self.get_image_uri(image_name, tag)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
import docker
|
|
235
|
+
|
|
236
|
+
client = docker.from_env()
|
|
237
|
+
|
|
238
|
+
logger.info(f"Pulling image {remote_uri}...")
|
|
239
|
+
client.images.pull(remote_uri)
|
|
240
|
+
logger.info(f"Successfully pulled {remote_uri}")
|
|
241
|
+
|
|
242
|
+
except ImportError:
|
|
243
|
+
result = subprocess.run(
|
|
244
|
+
["docker", "pull", remote_uri],
|
|
245
|
+
capture_output=True,
|
|
246
|
+
text=True,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if result.returncode != 0:
|
|
250
|
+
raise RuntimeError(f"Failed to pull image: {result.stderr}")
|
|
251
|
+
|
|
252
|
+
logger.info(f"Successfully pulled {remote_uri}")
|
|
253
|
+
|
|
254
|
+
def list_images(self) -> list[str]:
|
|
255
|
+
"""List images in the repository."""
|
|
256
|
+
self._ensure_initialized()
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
response = self._ecr_client.list_images(
|
|
260
|
+
repositoryName=self._repository,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
images = []
|
|
264
|
+
for image_id in response.get("imageIds", []):
|
|
265
|
+
if "imageTag" in image_id:
|
|
266
|
+
images.append(image_id["imageTag"])
|
|
267
|
+
|
|
268
|
+
return images
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.error(f"Failed to list images: {e}")
|
|
272
|
+
return []
|
|
273
|
+
|
|
274
|
+
def delete_image(self, image_name: str, tag: str = None, digest: str = None) -> bool:
|
|
275
|
+
"""Delete an image from ECR."""
|
|
276
|
+
self._ensure_initialized()
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
image_ids = []
|
|
280
|
+
if tag:
|
|
281
|
+
image_ids.append({"imageTag": tag})
|
|
282
|
+
elif digest:
|
|
283
|
+
image_ids.append({"imageDigest": digest})
|
|
284
|
+
else:
|
|
285
|
+
raise ValueError("Either tag or digest must be provided")
|
|
286
|
+
|
|
287
|
+
self._ecr_client.batch_delete_image(
|
|
288
|
+
repositoryName=self._repository,
|
|
289
|
+
imageIds=image_ids,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
logger.info(f"Deleted image {image_name}:{tag or digest}")
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(f"Failed to delete image: {e}")
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
def create_repository(self, repository_name: str = None) -> bool:
|
|
300
|
+
"""Create an ECR repository if it doesn't exist.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
repository_name: Repository name. Uses configured name if not provided.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
True if created or already exists.
|
|
307
|
+
"""
|
|
308
|
+
self._ensure_initialized()
|
|
309
|
+
|
|
310
|
+
repo_name = repository_name or self._repository
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
self._ecr_client.create_repository(repositoryName=repo_name)
|
|
314
|
+
logger.info(f"Created ECR repository: {repo_name}")
|
|
315
|
+
return True
|
|
316
|
+
except self._ecr_client.exceptions.RepositoryAlreadyExistsException:
|
|
317
|
+
logger.debug(f"Repository already exists: {repo_name}")
|
|
318
|
+
return True
|
|
319
|
+
except Exception as e:
|
|
320
|
+
logger.error(f"Failed to create repository: {e}")
|
|
321
|
+
return False
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""GCR/Artifact Registry - Native FlowyML Plugin.
|
|
2
|
+
|
|
3
|
+
This is a native FlowyML implementation for Google Container Registry
|
|
4
|
+
and Google Artifact Registry, without requiring any external framework.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from flowyml.plugins import get_plugin
|
|
8
|
+
|
|
9
|
+
registry = get_plugin("gcr",
|
|
10
|
+
project="my-gcp-project",
|
|
11
|
+
location="us-central1"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Push an image
|
|
15
|
+
uri = registry.push_image("my-ml-image", tag="v1.0")
|
|
16
|
+
|
|
17
|
+
# Get image URI
|
|
18
|
+
uri = registry.get_image_uri("my-ml-image", tag="latest")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import subprocess
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
from flowyml.plugins.base import ContainerRegistryPlugin, PluginMetadata, PluginType
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GCRRegistry(ContainerRegistryPlugin):
|
|
30
|
+
"""Native Google Container Registry / Artifact Registry for FlowyML.
|
|
31
|
+
|
|
32
|
+
This registry integrates directly with GCP container services
|
|
33
|
+
without any intermediate framework.
|
|
34
|
+
|
|
35
|
+
Supports both:
|
|
36
|
+
- Google Container Registry (gcr.io)
|
|
37
|
+
- Google Artifact Registry (recommended for new projects)
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
project: GCP project ID.
|
|
41
|
+
location: Region for Artifact Registry (e.g., "us-central1").
|
|
42
|
+
If not provided, uses gcr.io.
|
|
43
|
+
repository: Artifact Registry repository name (only for AR).
|
|
44
|
+
use_artifact_registry: If True, use Artifact Registry.
|
|
45
|
+
If False, use classic GCR.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
# Using Artifact Registry (recommended)
|
|
49
|
+
registry = GCRRegistry(
|
|
50
|
+
project="my-gcp-project",
|
|
51
|
+
location="us-central1",
|
|
52
|
+
repository="ml-images",
|
|
53
|
+
use_artifact_registry=True
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Using classic GCR
|
|
57
|
+
registry = GCRRegistry(
|
|
58
|
+
project="my-gcp-project",
|
|
59
|
+
use_artifact_registry=False
|
|
60
|
+
)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
METADATA = PluginMetadata(
|
|
64
|
+
name="gcr",
|
|
65
|
+
description="Google Container Registry / Artifact Registry",
|
|
66
|
+
plugin_type=PluginType.CONTAINER_REGISTRY,
|
|
67
|
+
version="1.0.0",
|
|
68
|
+
author="FlowyML",
|
|
69
|
+
packages=["google-cloud-artifact-registry>=1.0"],
|
|
70
|
+
documentation_url="https://cloud.google.com/artifact-registry/docs",
|
|
71
|
+
tags=["container-registry", "gcp", "cloud"],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
project: str,
|
|
77
|
+
location: str = None,
|
|
78
|
+
repository: str = None,
|
|
79
|
+
use_artifact_registry: bool = True,
|
|
80
|
+
**kwargs,
|
|
81
|
+
):
|
|
82
|
+
"""Initialize the GCR registry."""
|
|
83
|
+
super().__init__(
|
|
84
|
+
name=kwargs.pop("name", "gcr"),
|
|
85
|
+
project=project,
|
|
86
|
+
location=location,
|
|
87
|
+
repository=repository,
|
|
88
|
+
use_artifact_registry=use_artifact_registry,
|
|
89
|
+
**kwargs,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self._project = project
|
|
93
|
+
self._location = location
|
|
94
|
+
self._repository = repository
|
|
95
|
+
self._use_ar = use_artifact_registry
|
|
96
|
+
|
|
97
|
+
def initialize(self) -> None:
|
|
98
|
+
"""Initialize GCR/Artifact Registry connection."""
|
|
99
|
+
# Configure Docker to authenticate with GCR
|
|
100
|
+
try:
|
|
101
|
+
if self._use_ar:
|
|
102
|
+
# Artifact Registry authentication
|
|
103
|
+
result = subprocess.run(
|
|
104
|
+
["gcloud", "auth", "configure-docker", f"{self._location}-docker.pkg.dev", "--quiet"],
|
|
105
|
+
capture_output=True,
|
|
106
|
+
text=True,
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
# Classic GCR authentication
|
|
110
|
+
result = subprocess.run(
|
|
111
|
+
["gcloud", "auth", "configure-docker", "--quiet"],
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if result.returncode != 0:
|
|
117
|
+
logger.warning(f"Docker auth may not be configured: {result.stderr}")
|
|
118
|
+
|
|
119
|
+
self._is_initialized = True
|
|
120
|
+
logger.info(f"GCR registry initialized for project: {self._project}")
|
|
121
|
+
|
|
122
|
+
except FileNotFoundError:
|
|
123
|
+
logger.warning("gcloud CLI not found. Manual Docker authentication may be required.")
|
|
124
|
+
self._is_initialized = True
|
|
125
|
+
|
|
126
|
+
def _ensure_initialized(self) -> None:
|
|
127
|
+
"""Ensure the registry is initialized."""
|
|
128
|
+
if not self._is_initialized:
|
|
129
|
+
self.initialize()
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def registry_uri(self) -> str:
|
|
133
|
+
"""Get the base registry URI."""
|
|
134
|
+
if self._use_ar:
|
|
135
|
+
return f"{self._location}-docker.pkg.dev/{self._project}/{self._repository}"
|
|
136
|
+
else:
|
|
137
|
+
return f"gcr.io/{self._project}"
|
|
138
|
+
|
|
139
|
+
def get_image_uri(self, image_name: str, tag: str = "latest") -> str:
|
|
140
|
+
"""Get the full URI for an image.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
image_name: Name of the image.
|
|
144
|
+
tag: Image tag.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Full image URI.
|
|
148
|
+
"""
|
|
149
|
+
return f"{self.registry_uri}/{image_name}:{tag}"
|
|
150
|
+
|
|
151
|
+
def push_image(self, image_name: str, tag: str = "latest", local_image: str = None) -> str:
|
|
152
|
+
"""Push an image to the registry.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
image_name: Name for the image in the registry.
|
|
156
|
+
tag: Image tag.
|
|
157
|
+
local_image: Local image name to push. If not provided,
|
|
158
|
+
assumes the image is already tagged correctly.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Full image URI.
|
|
162
|
+
"""
|
|
163
|
+
self._ensure_initialized()
|
|
164
|
+
|
|
165
|
+
remote_uri = self.get_image_uri(image_name, tag)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
import docker
|
|
169
|
+
|
|
170
|
+
client = docker.from_env()
|
|
171
|
+
|
|
172
|
+
if local_image:
|
|
173
|
+
# Tag the local image with the remote URI
|
|
174
|
+
image = client.images.get(local_image)
|
|
175
|
+
image.tag(remote_uri)
|
|
176
|
+
|
|
177
|
+
# Push the image
|
|
178
|
+
logger.info(f"Pushing image to {remote_uri}...")
|
|
179
|
+
|
|
180
|
+
for line in client.images.push(remote_uri, stream=True, decode=True):
|
|
181
|
+
if "status" in line:
|
|
182
|
+
logger.debug(line["status"])
|
|
183
|
+
if "error" in line:
|
|
184
|
+
raise RuntimeError(line["error"])
|
|
185
|
+
|
|
186
|
+
logger.info(f"Successfully pushed {remote_uri}")
|
|
187
|
+
return remote_uri
|
|
188
|
+
|
|
189
|
+
except ImportError:
|
|
190
|
+
# Fall back to Docker CLI
|
|
191
|
+
logger.info("docker-py not available, using Docker CLI")
|
|
192
|
+
|
|
193
|
+
if local_image:
|
|
194
|
+
subprocess.run(
|
|
195
|
+
["docker", "tag", local_image, remote_uri],
|
|
196
|
+
check=True,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
result = subprocess.run(
|
|
200
|
+
["docker", "push", remote_uri],
|
|
201
|
+
capture_output=True,
|
|
202
|
+
text=True,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if result.returncode != 0:
|
|
206
|
+
raise RuntimeError(f"Failed to push image: {result.stderr}")
|
|
207
|
+
|
|
208
|
+
logger.info(f"Successfully pushed {remote_uri}")
|
|
209
|
+
return remote_uri
|
|
210
|
+
|
|
211
|
+
def pull_image(self, image_name: str, tag: str = "latest") -> None:
|
|
212
|
+
"""Pull an image from the registry.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
image_name: Name of the image.
|
|
216
|
+
tag: Image tag.
|
|
217
|
+
"""
|
|
218
|
+
self._ensure_initialized()
|
|
219
|
+
|
|
220
|
+
remote_uri = self.get_image_uri(image_name, tag)
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
import docker
|
|
224
|
+
|
|
225
|
+
client = docker.from_env()
|
|
226
|
+
|
|
227
|
+
logger.info(f"Pulling image {remote_uri}...")
|
|
228
|
+
client.images.pull(remote_uri)
|
|
229
|
+
logger.info(f"Successfully pulled {remote_uri}")
|
|
230
|
+
|
|
231
|
+
except ImportError:
|
|
232
|
+
# Fall back to Docker CLI
|
|
233
|
+
result = subprocess.run(
|
|
234
|
+
["docker", "pull", remote_uri],
|
|
235
|
+
capture_output=True,
|
|
236
|
+
text=True,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if result.returncode != 0:
|
|
240
|
+
raise RuntimeError(f"Failed to pull image: {result.stderr}")
|
|
241
|
+
|
|
242
|
+
logger.info(f"Successfully pulled {remote_uri}")
|
|
243
|
+
|
|
244
|
+
def list_images(self) -> list[str]:
|
|
245
|
+
"""List images in the registry.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of image names.
|
|
249
|
+
"""
|
|
250
|
+
self._ensure_initialized()
|
|
251
|
+
|
|
252
|
+
if not self._use_ar:
|
|
253
|
+
logger.warning("Image listing requires Artifact Registry")
|
|
254
|
+
return []
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
from google.cloud import artifactregistry_v1
|
|
258
|
+
|
|
259
|
+
client = artifactregistry_v1.ArtifactRegistryClient()
|
|
260
|
+
|
|
261
|
+
parent = f"projects/{self._project}/locations/{self._location}/repositories/{self._repository}"
|
|
262
|
+
|
|
263
|
+
images = []
|
|
264
|
+
for image in client.list_docker_images(parent=parent):
|
|
265
|
+
# Extract image name from full path
|
|
266
|
+
name = image.name.split("/")[-1]
|
|
267
|
+
images.append(name)
|
|
268
|
+
|
|
269
|
+
return images
|
|
270
|
+
|
|
271
|
+
except ImportError:
|
|
272
|
+
logger.warning("google-cloud-artifact-registry not installed")
|
|
273
|
+
return []
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"Failed to list images: {e}")
|
|
276
|
+
return []
|
|
277
|
+
|
|
278
|
+
def delete_image(self, image_name: str, tag: str = None, digest: str = None) -> bool:
|
|
279
|
+
"""Delete an image from the registry.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
image_name: Name of the image.
|
|
283
|
+
tag: Image tag to delete.
|
|
284
|
+
digest: Image digest to delete.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
True if deletion was successful.
|
|
288
|
+
"""
|
|
289
|
+
self._ensure_initialized()
|
|
290
|
+
|
|
291
|
+
if not self._use_ar:
|
|
292
|
+
logger.warning("Image deletion via API requires Artifact Registry")
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
from google.cloud import artifactregistry_v1
|
|
297
|
+
|
|
298
|
+
client = artifactregistry_v1.ArtifactRegistryClient()
|
|
299
|
+
|
|
300
|
+
if digest:
|
|
301
|
+
name = f"projects/{self._project}/locations/{self._location}/repositories/{self._repository}/dockerImages/{image_name}@{digest}"
|
|
302
|
+
elif tag:
|
|
303
|
+
name = f"projects/{self._project}/locations/{self._location}/repositories/{self._repository}/dockerImages/{image_name}:{tag}"
|
|
304
|
+
else:
|
|
305
|
+
raise ValueError("Either tag or digest must be provided")
|
|
306
|
+
|
|
307
|
+
client.delete_package(name=name)
|
|
308
|
+
logger.info(f"Deleted image {name}")
|
|
309
|
+
return True
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Failed to delete image: {e}")
|
|
313
|
+
return False
|