zenml-nightly 0.61.0.dev20240711__py3-none-any.whl → 0.61.0.dev20240713__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.
Files changed (27) hide show
  1. zenml/VERSION +1 -1
  2. zenml/cli/stack.py +220 -71
  3. zenml/constants.py +6 -0
  4. zenml/enums.py +16 -0
  5. zenml/integrations/gcp/service_connectors/gcp_service_connector.py +54 -6
  6. zenml/logging/step_logging.py +34 -35
  7. zenml/models/__init__.py +2 -0
  8. zenml/models/v2/core/server_settings.py +0 -20
  9. zenml/models/v2/misc/stack_deployment.py +20 -0
  10. zenml/orchestrators/step_launcher.py +1 -0
  11. zenml/stack_deployments/aws_stack_deployment.py +56 -91
  12. zenml/stack_deployments/gcp_stack_deployment.py +260 -0
  13. zenml/stack_deployments/stack_deployment.py +103 -25
  14. zenml/stack_deployments/utils.py +4 -0
  15. zenml/zen_server/routers/devices_endpoints.py +4 -1
  16. zenml/zen_server/routers/server_endpoints.py +29 -2
  17. zenml/zen_server/routers/stack_deployment_endpoints.py +34 -20
  18. zenml/zen_stores/migrations/versions/b4fca5241eea_migrate_onboarding_state.py +167 -0
  19. zenml/zen_stores/rest_zen_store.py +45 -21
  20. zenml/zen_stores/schemas/server_settings_schemas.py +23 -11
  21. zenml/zen_stores/sql_zen_store.py +117 -19
  22. zenml/zen_stores/zen_store_interface.py +6 -5
  23. {zenml_nightly-0.61.0.dev20240711.dist-info → zenml_nightly-0.61.0.dev20240713.dist-info}/METADATA +1 -1
  24. {zenml_nightly-0.61.0.dev20240711.dist-info → zenml_nightly-0.61.0.dev20240713.dist-info}/RECORD +27 -25
  25. {zenml_nightly-0.61.0.dev20240711.dist-info → zenml_nightly-0.61.0.dev20240713.dist-info}/LICENSE +0 -0
  26. {zenml_nightly-0.61.0.dev20240711.dist-info → zenml_nightly-0.61.0.dev20240713.dist-info}/WHEEL +0 -0
  27. {zenml_nightly-0.61.0.dev20240711.dist-info → zenml_nightly-0.61.0.dev20240713.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,167 @@
1
+ """Migrate onboarding state [b4fca5241eea].
2
+
3
+ Revision ID: b4fca5241eea
4
+ Revises: 0.61.0
5
+ Create Date: 2024-06-20 15:01:22.414801
6
+
7
+ """
8
+
9
+ import json
10
+ from typing import Dict, List
11
+
12
+ import sqlalchemy as sa
13
+ from alembic import op
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision = "b4fca5241eea"
17
+ down_revision = "0.61.0"
18
+ branch_labels = None
19
+ depends_on = None
20
+
21
+
22
+ ONBOARDING_KEY_MAPPING = {
23
+ "connect_zenml": ["device_verified"],
24
+ "run_first_pipeline": ["pipeline_run", "starter_setup_completed"],
25
+ "run_remote_pipeline": [
26
+ "production_setup_completed",
27
+ ],
28
+ }
29
+
30
+
31
+ def upgrade() -> None:
32
+ """Upgrade database schema and/or data, creating a new revision."""
33
+ connection = op.get_bind()
34
+
35
+ meta = sa.MetaData()
36
+ meta.reflect(only=("server_settings",), bind=connection)
37
+
38
+ server_settings_table = sa.Table("server_settings", meta)
39
+
40
+ existing_onboarding_state = connection.execute(
41
+ sa.select(server_settings_table.c.onboarding_state)
42
+ ).scalar_one_or_none()
43
+
44
+ new_state = []
45
+
46
+ if existing_onboarding_state:
47
+ # There was already an existing onboarding state in the DB
48
+ # -> Migrate to the new server keys
49
+ state = json.loads(existing_onboarding_state)
50
+
51
+ if isinstance(state, Dict):
52
+ for key in state.keys():
53
+ if key in ONBOARDING_KEY_MAPPING:
54
+ new_state.extend(ONBOARDING_KEY_MAPPING[key])
55
+ elif isinstance(state, List):
56
+ # Somehow the state is already converted, probably shouldn't happen
57
+ return
58
+
59
+ # We now query the DB and complete all onboarding steps that we can detect
60
+ # from the database
61
+ meta = sa.MetaData()
62
+ meta.reflect(
63
+ only=(
64
+ "pipeline_run",
65
+ "stack_component",
66
+ "stack",
67
+ "stack_composition",
68
+ "pipeline_deployment",
69
+ ),
70
+ bind=connection,
71
+ )
72
+
73
+ pipeline_run_table = sa.Table("pipeline_run", meta)
74
+ stack_component_table = sa.Table("stack_component", meta)
75
+ stack_table = sa.Table("stack", meta)
76
+ stack_composition_table = sa.Table("stack_composition", meta)
77
+ pipeline_deployment_table = sa.Table("pipeline_deployment", meta)
78
+
79
+ pipeline_run_count = connection.execute(
80
+ sa.select(sa.func.count(pipeline_run_table.c.id))
81
+ ).scalar()
82
+ if pipeline_run_count and pipeline_run_count > 0:
83
+ new_state.extend(ONBOARDING_KEY_MAPPING["run_first_pipeline"])
84
+
85
+ stack_with_remote_orchestrator_count = connection.execute(
86
+ sa.select(sa.func.count(stack_table.c.id))
87
+ .where(stack_composition_table.c.stack_id == stack_table.c.id)
88
+ .where(
89
+ stack_composition_table.c.component_id
90
+ == stack_component_table.c.id
91
+ )
92
+ .where(
93
+ stack_component_table.c.flavor.not_in(["local", "local_docker"])
94
+ )
95
+ .where(stack_component_table.c.type == "orchestrator")
96
+ ).scalar()
97
+ if (
98
+ stack_with_remote_orchestrator_count
99
+ and stack_with_remote_orchestrator_count > 0
100
+ ):
101
+ new_state.append("stack_with_remote_orchestrator_created")
102
+
103
+ pipeline_run_with_remote_artifact_store_count = connection.execute(
104
+ sa.select(sa.func.count(pipeline_run_table.c.id))
105
+ .where(
106
+ pipeline_run_table.c.deployment_id
107
+ == pipeline_deployment_table.c.id
108
+ )
109
+ .where(pipeline_deployment_table.c.stack_id == stack_table.c.id)
110
+ .where(stack_composition_table.c.stack_id == stack_table.c.id)
111
+ .where(
112
+ stack_composition_table.c.component_id
113
+ == stack_component_table.c.id
114
+ )
115
+ .where(stack_component_table.c.flavor != "local")
116
+ .where(stack_component_table.c.type == "artifact_store")
117
+ ).scalar()
118
+ if (
119
+ pipeline_run_with_remote_artifact_store_count
120
+ and pipeline_run_with_remote_artifact_store_count > 0
121
+ ):
122
+ new_state.append("production_setup_completed")
123
+
124
+ pipeline_run_with_remote_orchestrator_count = connection.execute(
125
+ sa.select(sa.func.count(pipeline_run_table.c.id))
126
+ .where(
127
+ pipeline_run_table.c.deployment_id
128
+ == pipeline_deployment_table.c.id
129
+ )
130
+ .where(pipeline_deployment_table.c.stack_id == stack_table.c.id)
131
+ .where(stack_composition_table.c.stack_id == stack_table.c.id)
132
+ .where(
133
+ stack_composition_table.c.component_id
134
+ == stack_component_table.c.id
135
+ )
136
+ .where(
137
+ stack_component_table.c.flavor.not_in(["local", "local_docker"])
138
+ )
139
+ .where(stack_component_table.c.type == "orchestrator")
140
+ ).scalar()
141
+ if (
142
+ pipeline_run_with_remote_orchestrator_count
143
+ and pipeline_run_with_remote_orchestrator_count > 0
144
+ ):
145
+ new_state.append("pipeline_run_with_remote_orchestrator")
146
+ new_state.append("production_setup_completed")
147
+
148
+ if new_state:
149
+ # If any of the items are finished, we also complete the initial
150
+ # onboarding step which is not explicitly tracked in the database
151
+ new_state.append("device_verified")
152
+
153
+ # Remove duplicate keys
154
+ new_state = list(set(new_state))
155
+
156
+ connection.execute(
157
+ sa.update(server_settings_table).values(
158
+ onboarding_state=json.dumps(new_state)
159
+ )
160
+ )
161
+
162
+
163
+ def downgrade() -> None:
164
+ """Downgrade database schema and/or data back to the previous revision."""
165
+ # ### commands auto generated by Alembic - please adjust! ###
166
+ pass
167
+ # ### end Alembic commands ###
@@ -58,6 +58,7 @@ from zenml.constants import (
58
58
  ARTIFACTS,
59
59
  CODE_REFERENCES,
60
60
  CODE_REPOSITORIES,
61
+ CONFIG,
61
62
  CURRENT_USER,
62
63
  DEACTIVATE,
63
64
  DEFAULT_HTTP_TIMEOUT,
@@ -91,6 +92,7 @@ from zenml.constants import (
91
92
  SERVICE_CONNECTOR_RESOURCES,
92
93
  SERVICE_CONNECTOR_TYPES,
93
94
  SERVICE_CONNECTOR_VERIFY,
95
+ SERVICE_CONNECTOR_VERIFY_REQUEST_TIMEOUT,
94
96
  SERVICE_CONNECTORS,
95
97
  SERVICES,
96
98
  STACK,
@@ -101,7 +103,6 @@ from zenml.constants import (
101
103
  TAGS,
102
104
  TRIGGER_EXECUTIONS,
103
105
  TRIGGERS,
104
- URL,
105
106
  USERS,
106
107
  VERSION_1,
107
108
  WORKSPACES,
@@ -216,6 +217,7 @@ from zenml.models import (
216
217
  ServiceRequest,
217
218
  ServiceResponse,
218
219
  ServiceUpdate,
220
+ StackDeploymentConfig,
219
221
  StackDeploymentInfo,
220
222
  StackFilter,
221
223
  StackRequest,
@@ -2476,6 +2478,10 @@ class RestZenStore(BaseZenStore):
2476
2478
  f"{SERVICE_CONNECTORS}{SERVICE_CONNECTOR_VERIFY}",
2477
2479
  body=service_connector,
2478
2480
  params={"list_resources": list_resources},
2481
+ timeout=max(
2482
+ self.config.http_timeout,
2483
+ SERVICE_CONNECTOR_VERIFY_REQUEST_TIMEOUT,
2484
+ ),
2479
2485
  )
2480
2486
 
2481
2487
  resources = ServiceConnectorResourcesModel.model_validate(
@@ -2513,6 +2519,10 @@ class RestZenStore(BaseZenStore):
2513
2519
  response_body = self.put(
2514
2520
  f"{SERVICE_CONNECTORS}/{str(service_connector_id)}{SERVICE_CONNECTOR_VERIFY}",
2515
2521
  params=params,
2522
+ timeout=max(
2523
+ self.config.http_timeout,
2524
+ SERVICE_CONNECTOR_VERIFY_REQUEST_TIMEOUT,
2525
+ ),
2516
2526
  )
2517
2527
 
2518
2528
  resources = ServiceConnectorResourcesModel.model_validate(
@@ -2859,13 +2869,13 @@ class RestZenStore(BaseZenStore):
2859
2869
  )
2860
2870
  return StackDeploymentInfo.model_validate(body)
2861
2871
 
2862
- def get_stack_deployment_url(
2872
+ def get_stack_deployment_config(
2863
2873
  self,
2864
2874
  provider: StackDeploymentProvider,
2865
2875
  stack_name: str,
2866
2876
  location: Optional[str] = None,
2867
- ) -> Tuple[str, str]:
2868
- """Return the URL to deploy the ZenML stack to the specified cloud provider.
2877
+ ) -> StackDeploymentConfig:
2878
+ """Return the cloud provider console URL and configuration needed to deploy the ZenML stack.
2869
2879
 
2870
2880
  Args:
2871
2881
  provider: The stack deployment provider.
@@ -2873,11 +2883,8 @@ class RestZenStore(BaseZenStore):
2873
2883
  location: The location where the stack should be deployed.
2874
2884
 
2875
2885
  Returns:
2876
- The URL to deploy the ZenML stack to the specified cloud provider
2877
- and a text description of the URL.
2878
-
2879
- Raises:
2880
- ValueError: If the response body is not as expected.
2886
+ The cloud provider console URL and configuration needed to deploy
2887
+ the ZenML stack to the specified cloud provider.
2881
2888
  """
2882
2889
  params = {
2883
2890
  "provider": provider.value,
@@ -2885,14 +2892,8 @@ class RestZenStore(BaseZenStore):
2885
2892
  }
2886
2893
  if location:
2887
2894
  params["location"] = location
2888
- body = self.get(f"{STACK_DEPLOYMENT}{URL}", params=params)
2889
-
2890
- if not isinstance(body, list) or len(body) != 2:
2891
- raise ValueError(
2892
- "Bad response body received from the stack deployment URL "
2893
- "endpoint."
2894
- )
2895
- return body[0], body[1]
2895
+ body = self.get(f"{STACK_DEPLOYMENT}{CONFIG}", params=params)
2896
+ return StackDeploymentConfig.model_validate(body)
2896
2897
 
2897
2898
  def get_stack_deployment_stack(
2898
2899
  self,
@@ -4081,6 +4082,7 @@ class RestZenStore(BaseZenStore):
4081
4082
  method: str,
4082
4083
  url: str,
4083
4084
  params: Optional[Dict[str, Any]] = None,
4085
+ timeout: Optional[int] = None,
4084
4086
  **kwargs: Any,
4085
4087
  ) -> Json:
4086
4088
  """Make a request to the REST API.
@@ -4089,6 +4091,7 @@ class RestZenStore(BaseZenStore):
4089
4091
  method: The HTTP method to use.
4090
4092
  url: The URL to request.
4091
4093
  params: The query parameters to pass to the endpoint.
4094
+ timeout: The request timeout in seconds.
4092
4095
  kwargs: Additional keyword arguments to pass to the request.
4093
4096
 
4094
4097
  Returns:
@@ -4111,7 +4114,7 @@ class RestZenStore(BaseZenStore):
4111
4114
  url,
4112
4115
  params=params,
4113
4116
  verify=self.config.verify_ssl,
4114
- timeout=self.config.http_timeout,
4117
+ timeout=timeout or self.config.http_timeout,
4115
4118
  **kwargs,
4116
4119
  )
4117
4120
  )
@@ -4140,13 +4143,18 @@ class RestZenStore(BaseZenStore):
4140
4143
  raise
4141
4144
 
4142
4145
  def get(
4143
- self, path: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any
4146
+ self,
4147
+ path: str,
4148
+ params: Optional[Dict[str, Any]] = None,
4149
+ timeout: Optional[int] = None,
4150
+ **kwargs: Any,
4144
4151
  ) -> Json:
4145
4152
  """Make a GET request to the given endpoint path.
4146
4153
 
4147
4154
  Args:
4148
4155
  path: The path to the endpoint.
4149
4156
  params: The query parameters to pass to the endpoint.
4157
+ timeout: The request timeout in seconds.
4150
4158
  kwargs: Additional keyword arguments to pass to the request.
4151
4159
 
4152
4160
  Returns:
@@ -4154,17 +4162,26 @@ class RestZenStore(BaseZenStore):
4154
4162
  """
4155
4163
  logger.debug(f"Sending GET request to {path}...")
4156
4164
  return self._request(
4157
- "GET", self.url + API + VERSION_1 + path, params=params, **kwargs
4165
+ "GET",
4166
+ self.url + API + VERSION_1 + path,
4167
+ params=params,
4168
+ timeout=timeout,
4169
+ **kwargs,
4158
4170
  )
4159
4171
 
4160
4172
  def delete(
4161
- self, path: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any
4173
+ self,
4174
+ path: str,
4175
+ params: Optional[Dict[str, Any]] = None,
4176
+ timeout: Optional[int] = None,
4177
+ **kwargs: Any,
4162
4178
  ) -> Json:
4163
4179
  """Make a DELETE request to the given endpoint path.
4164
4180
 
4165
4181
  Args:
4166
4182
  path: The path to the endpoint.
4167
4183
  params: The query parameters to pass to the endpoint.
4184
+ timeout: The request timeout in seconds.
4168
4185
  kwargs: Additional keyword arguments to pass to the request.
4169
4186
 
4170
4187
  Returns:
@@ -4175,6 +4192,7 @@ class RestZenStore(BaseZenStore):
4175
4192
  "DELETE",
4176
4193
  self.url + API + VERSION_1 + path,
4177
4194
  params=params,
4195
+ timeout=timeout,
4178
4196
  **kwargs,
4179
4197
  )
4180
4198
 
@@ -4183,6 +4201,7 @@ class RestZenStore(BaseZenStore):
4183
4201
  path: str,
4184
4202
  body: BaseModel,
4185
4203
  params: Optional[Dict[str, Any]] = None,
4204
+ timeout: Optional[int] = None,
4186
4205
  **kwargs: Any,
4187
4206
  ) -> Json:
4188
4207
  """Make a POST request to the given endpoint path.
@@ -4191,6 +4210,7 @@ class RestZenStore(BaseZenStore):
4191
4210
  path: The path to the endpoint.
4192
4211
  body: The body to send.
4193
4212
  params: The query parameters to pass to the endpoint.
4213
+ timeout: The request timeout in seconds.
4194
4214
  kwargs: Additional keyword arguments to pass to the request.
4195
4215
 
4196
4216
  Returns:
@@ -4202,6 +4222,7 @@ class RestZenStore(BaseZenStore):
4202
4222
  self.url + API + VERSION_1 + path,
4203
4223
  data=body.model_dump_json(),
4204
4224
  params=params,
4225
+ timeout=timeout,
4205
4226
  **kwargs,
4206
4227
  )
4207
4228
 
@@ -4210,6 +4231,7 @@ class RestZenStore(BaseZenStore):
4210
4231
  path: str,
4211
4232
  body: Optional[BaseModel] = None,
4212
4233
  params: Optional[Dict[str, Any]] = None,
4234
+ timeout: Optional[int] = None,
4213
4235
  **kwargs: Any,
4214
4236
  ) -> Json:
4215
4237
  """Make a PUT request to the given endpoint path.
@@ -4218,6 +4240,7 @@ class RestZenStore(BaseZenStore):
4218
4240
  path: The path to the endpoint.
4219
4241
  body: The body to send.
4220
4242
  params: The query parameters to pass to the endpoint.
4243
+ timeout: The request timeout in seconds.
4221
4244
  kwargs: Additional keyword arguments to pass to the request.
4222
4245
 
4223
4246
  Returns:
@@ -4230,6 +4253,7 @@ class RestZenStore(BaseZenStore):
4230
4253
  self.url + API + VERSION_1 + path,
4231
4254
  data=data,
4232
4255
  params=params,
4256
+ timeout=timeout,
4233
4257
  **kwargs,
4234
4258
  )
4235
4259
 
@@ -15,7 +15,7 @@
15
15
 
16
16
  import json
17
17
  from datetime import datetime
18
- from typing import Any, Optional
18
+ from typing import Any, Optional, Set
19
19
  from uuid import UUID
20
20
 
21
21
  from sqlmodel import Field, SQLModel
@@ -59,16 +59,33 @@ class ServerSettingsSchema(SQLModel, table=True):
59
59
  for field, value in settings_update.model_dump(
60
60
  exclude_unset=True
61
61
  ).items():
62
- if field == "onboarding_state":
63
- if value is not None:
64
- self.onboarding_state = json.dumps(value)
65
- elif hasattr(self, field):
62
+ if hasattr(self, field):
66
63
  setattr(self, field, value)
67
64
 
68
65
  self.updated = datetime.utcnow()
69
66
 
70
67
  return self
71
68
 
69
+ def update_onboarding_state(
70
+ self, completed_steps: Set[str]
71
+ ) -> "ServerSettingsSchema":
72
+ """Update the onboarding state.
73
+
74
+ Args:
75
+ completed_steps: Newly completed onboarding steps.
76
+
77
+ Returns:
78
+ The updated schema.
79
+ """
80
+ old_state = set(
81
+ json.loads(self.onboarding_state) if self.onboarding_state else []
82
+ )
83
+ new_state = old_state.union(completed_steps)
84
+ self.onboarding_state = json.dumps(list(new_state))
85
+ self.updated = datetime.utcnow()
86
+
87
+ return self
88
+
72
89
  def to_model(
73
90
  self,
74
91
  include_metadata: bool = False,
@@ -82,7 +99,6 @@ class ServerSettingsSchema(SQLModel, table=True):
82
99
  include_resources: Whether the resources will be filled.
83
100
  **kwargs: Keyword arguments to allow schema specific logic
84
101
 
85
-
86
102
  Returns:
87
103
  The created `SettingsResponse`.
88
104
  """
@@ -101,11 +117,7 @@ class ServerSettingsSchema(SQLModel, table=True):
101
117
  resources = None
102
118
 
103
119
  if include_metadata:
104
- metadata = ServerSettingsResponseMetadata(
105
- onboarding_state=json.loads(self.onboarding_state)
106
- if self.onboarding_state
107
- else {},
108
- )
120
+ metadata = ServerSettingsResponseMetadata()
109
121
 
110
122
  if include_resources:
111
123
  resources = ServerSettingsResponseResources()
@@ -33,6 +33,7 @@ from typing import (
33
33
  NoReturn,
34
34
  Optional,
35
35
  Sequence,
36
+ Set,
36
37
  Tuple,
37
38
  Type,
38
39
  TypeVar,
@@ -107,6 +108,7 @@ from zenml.enums import (
107
108
  ExecutionStatus,
108
109
  LoggingLevels,
109
110
  ModelStages,
111
+ OnboardingStep,
110
112
  SecretScope,
111
113
  SecretsStoreType,
112
114
  SorterOps,
@@ -245,6 +247,7 @@ from zenml.models import (
245
247
  ServiceRequest,
246
248
  ServiceResponse,
247
249
  ServiceUpdate,
250
+ StackDeploymentConfig,
248
251
  StackDeploymentInfo,
249
252
  StackFilter,
250
253
  StackRequest,
@@ -796,6 +799,7 @@ class SqlZenStore(BaseZenStore):
796
799
  _secrets_store: Optional[BaseSecretsStore] = None
797
800
  _backup_secrets_store: Optional[BaseSecretsStore] = None
798
801
  _should_send_user_enriched_events: bool = False
802
+ _cached_onboarding_state: Optional[Set[str]] = None
799
803
 
800
804
  @property
801
805
  def secrets_store(self) -> "BaseSecretsStore":
@@ -1552,12 +1556,18 @@ class SqlZenStore(BaseZenStore):
1552
1556
  revisions_afterwards = self.alembic.current_revisions()
1553
1557
 
1554
1558
  if current_revisions != revisions_afterwards:
1555
- if current_revisions and version.parse(
1556
- current_revisions[0]
1557
- ) < version.parse("0.57.1"):
1558
- # We want to send the missing user enriched events for users
1559
- # which were created pre 0.57.1 and only on one upgrade
1560
- self._should_send_user_enriched_events = True
1559
+ try:
1560
+ if current_revisions and version.parse(
1561
+ current_revisions[0]
1562
+ ) < version.parse("0.57.1"):
1563
+ # We want to send the missing user enriched events for users
1564
+ # which were created pre 0.57.1 and only on one upgrade
1565
+ self._should_send_user_enriched_events = True
1566
+ except version.InvalidVersion:
1567
+ # This can happen if the database is not currently
1568
+ # stamped with an official ZenML version (e.g. in
1569
+ # development environments).
1570
+ pass
1561
1571
 
1562
1572
  self._sync_flavors()
1563
1573
 
@@ -1681,6 +1691,60 @@ class SqlZenStore(BaseZenStore):
1681
1691
 
1682
1692
  return settings.to_model(include_metadata=True)
1683
1693
 
1694
+ def get_onboarding_state(self) -> List[str]:
1695
+ """Get the server onboarding state.
1696
+
1697
+ Returns:
1698
+ The server onboarding state.
1699
+ """
1700
+ with Session(self.engine) as session:
1701
+ settings = self._get_server_settings(session=session)
1702
+ if settings.onboarding_state:
1703
+ self._cached_onboarding_state = set(
1704
+ json.loads(settings.onboarding_state)
1705
+ )
1706
+ return list(self._cached_onboarding_state)
1707
+ else:
1708
+ return []
1709
+
1710
+ def _update_onboarding_state(
1711
+ self, completed_steps: Set[str], session: Session
1712
+ ) -> None:
1713
+ """Update the server onboarding state.
1714
+
1715
+ Args:
1716
+ completed_steps: Newly completed onboarding steps.
1717
+ session: DB session.
1718
+ """
1719
+ if self._cached_onboarding_state and completed_steps.issubset(
1720
+ self._cached_onboarding_state
1721
+ ):
1722
+ # All the onboarding steps are already completed, no need to query
1723
+ # the DB
1724
+ return
1725
+
1726
+ settings = self._get_server_settings(session=session)
1727
+ settings.update_onboarding_state(completed_steps=completed_steps)
1728
+ session.add(settings)
1729
+ session.commit()
1730
+ session.refresh(settings)
1731
+
1732
+ assert settings.onboarding_state
1733
+ self._cached_onboarding_state = set(
1734
+ json.loads(settings.onboarding_state)
1735
+ )
1736
+
1737
+ def update_onboarding_state(self, completed_steps: Set[str]) -> None:
1738
+ """Update the server onboarding state.
1739
+
1740
+ Args:
1741
+ completed_steps: Newly completed onboarding steps.
1742
+ """
1743
+ with Session(self.engine) as session:
1744
+ self._update_onboarding_state(
1745
+ completed_steps=completed_steps, session=session
1746
+ )
1747
+
1684
1748
  def activate_server(
1685
1749
  self, request: ServerActivationRequest
1686
1750
  ) -> Optional[UserResponse]:
@@ -6149,6 +6213,7 @@ class SqlZenStore(BaseZenStore):
6149
6213
 
6150
6214
  connector = new_service_connector.to_model(include_metadata=True)
6151
6215
  self._populate_connector_type(connector)
6216
+
6152
6217
  return connector
6153
6218
 
6154
6219
  def get_service_connector(
@@ -6924,6 +6989,16 @@ class SqlZenStore(BaseZenStore):
6924
6989
  session.commit()
6925
6990
  session.refresh(new_stack_schema)
6926
6991
 
6992
+ for component in defined_components:
6993
+ if component.type == StackComponentType.ORCHESTRATOR:
6994
+ if component.flavor not in {"local", "local_docker"}:
6995
+ self._update_onboarding_state(
6996
+ completed_steps={
6997
+ OnboardingStep.STACK_WITH_REMOTE_ORCHESTRATOR_CREATED
6998
+ },
6999
+ session=session,
7000
+ )
7001
+
6927
7002
  return new_stack_schema.to_model(include_metadata=True)
6928
7003
 
6929
7004
  def create_full_stack(self, full_stack: FullStackRequest) -> StackResponse:
@@ -7000,7 +7075,6 @@ class SqlZenStore(BaseZenStore):
7000
7075
 
7001
7076
  # Stack Components
7002
7077
  components_mapping: Dict[StackComponentType, List[UUID]] = {}
7003
-
7004
7078
  for (
7005
7079
  component_type,
7006
7080
  component_info,
@@ -7012,6 +7086,20 @@ class SqlZenStore(BaseZenStore):
7012
7086
  )
7013
7087
  # Create a new component
7014
7088
  else:
7089
+ flavor_list = self.list_flavors(
7090
+ flavor_filter_model=FlavorFilter(
7091
+ name=component_info.flavor,
7092
+ type=component_type,
7093
+ )
7094
+ )
7095
+ if not len(flavor_list):
7096
+ raise ValueError(
7097
+ f"Flavor '{component_info.flavor}' not found "
7098
+ f"for component type '{component_type}'."
7099
+ )
7100
+
7101
+ flavor_model = flavor_list[0]
7102
+
7015
7103
  component_name = full_stack.name
7016
7104
  while True:
7017
7105
  try:
@@ -7039,15 +7127,6 @@ class SqlZenStore(BaseZenStore):
7039
7127
  service_connector = service_connectors[
7040
7128
  component_info.service_connector_index
7041
7129
  ]
7042
- flavor_list = self.list_flavors(
7043
- flavor_filter_model=FlavorFilter(
7044
- name=component_info.flavor,
7045
- type=component_type,
7046
- )
7047
- )
7048
- assert len(flavor_list) == 1
7049
-
7050
- flavor_model = flavor_list[0]
7051
7130
 
7052
7131
  requirements = flavor_model.connector_requirements
7053
7132
 
@@ -7434,13 +7513,13 @@ class SqlZenStore(BaseZenStore):
7434
7513
  "Stack deployments are not supported by local ZenML deployments."
7435
7514
  )
7436
7515
 
7437
- def get_stack_deployment_url(
7516
+ def get_stack_deployment_config(
7438
7517
  self,
7439
7518
  provider: StackDeploymentProvider,
7440
7519
  stack_name: str,
7441
7520
  location: Optional[str] = None,
7442
- ) -> Tuple[str, str]:
7443
- """Return the URL to deploy the ZenML stack to the specified cloud provider.
7521
+ ) -> StackDeploymentConfig:
7522
+ """Return the cloud provider console URL and configuration needed to deploy the ZenML stack.
7444
7523
 
7445
7524
  Args:
7446
7525
  provider: The stack deployment provider.
@@ -7954,6 +8033,25 @@ class SqlZenStore(BaseZenStore):
7954
8033
  "duration_seconds": duration_seconds,
7955
8034
  **stack_metadata,
7956
8035
  }
8036
+
8037
+ completed_onboarding_steps: Set[str] = {
8038
+ OnboardingStep.PIPELINE_RUN,
8039
+ OnboardingStep.STARTER_SETUP_COMPLETED,
8040
+ }
8041
+ if stack_metadata["orchestrator"] not in {
8042
+ "local",
8043
+ "local_docker",
8044
+ }:
8045
+ completed_onboarding_steps.update(
8046
+ {
8047
+ OnboardingStep.PIPELINE_RUN_WITH_REMOTE_ORCHESTRATOR,
8048
+ OnboardingStep.PRODUCTION_SETUP_COMPLETED,
8049
+ }
8050
+ )
8051
+
8052
+ self._update_onboarding_state(
8053
+ completed_steps=completed_onboarding_steps, session=session
8054
+ )
7957
8055
  pipeline_run.update(run_update)
7958
8056
  session.add(pipeline_run)
7959
8057