Skip to content

Commit

Permalink
Added snow spcs service deploy command (#2026)
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-astus authored Jan 24, 2025
1 parent 774c9de commit d2fe300
Show file tree
Hide file tree
Showing 18 changed files with 765 additions and 23 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
* Added command `snow spcs compute-pool deploy`.
* Added support for Mac Os x86_64 architecture.
* Added image repository model in snowflake.yml.
* Added `snow spcs service deploy` command.

## Fixes and improvements

Expand Down
40 changes: 40 additions & 0 deletions src/snowflake/cli/_plugins/spcs/services/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,23 @@
validate_and_set_instances,
)
from snowflake.cli._plugins.spcs.services.manager import ServiceManager
from snowflake.cli._plugins.spcs.services.service_project_paths import (
ServiceProjectPaths,
)
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.commands.flags import (
IfNotExistsOption,
OverrideableOption,
ReplaceOption,
entity_argument,
identifier_argument,
like_option,
)
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
from snowflake.cli.api.constants import ObjectType
from snowflake.cli.api.exceptions import (
IncompatibleParametersError,
NoProjectDefinitionError,
)
from snowflake.cli.api.feature_flags import FeatureFlag
from snowflake.cli.api.identifiers import FQN
Expand All @@ -51,6 +58,7 @@
SingleQueryResult,
StreamResult,
)
from snowflake.cli.api.project.definition_helper import get_entity
from snowflake.cli.api.project.util import is_valid_object_name

app = SnowTyperFactory(
Expand Down Expand Up @@ -200,6 +208,38 @@ def create(
return SingleQueryResult(cursor)


@app.command(requires_connection=True)
def deploy(
replace: bool = ReplaceOption(help="Replace the service if it already exists."),
entity_id: str = entity_argument("service"),
**options,
) -> CommandResult:
"""
Deploys a service defined in the project definition file.
"""
cli_context = get_cli_context()
pd = cli_context.project_definition

if pd is None:
raise NoProjectDefinitionError(
project_type="service", project_root=cli_context.project_root
)

service = get_entity(
pd=pd,
project_root=cli_context.project_root,
entity_type=ObjectType.SERVICE,
entity_id=entity_id,
)
service_project_paths = ServiceProjectPaths(cli_context.project_root)
cursor = ServiceManager().deploy(
service=service,
service_project_paths=service_project_paths,
replace=replace,
)
return SingleQueryResult(cursor)


@app.command(requires_connection=True)
def execute_job(
name: FQN = ServiceNameArgument,
Expand Down
115 changes: 114 additions & 1 deletion src/snowflake/cli/_plugins/spcs/services/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
import json
import time
from datetime import datetime
from pathlib import Path
from pathlib import Path, PurePosixPath
from typing import List, Optional

import yaml
from snowflake.cli._plugins.object.common import Tag
from snowflake.cli._plugins.object.manager import ObjectManager
from snowflake.cli._plugins.spcs.common import (
EVENT_COLUMN_NAMES,
NoPropertiesProvidedError,
Expand All @@ -35,7 +36,16 @@
new_logs_only,
strip_empty_lines,
)
from snowflake.cli._plugins.spcs.services.service_entity_model import ServiceEntityModel
from snowflake.cli._plugins.spcs.services.service_project_paths import (
ServiceProjectPaths,
)
from snowflake.cli._plugins.stage.manager import StageManager
from snowflake.cli.api.artifacts.bundle_map import BundleMap
from snowflake.cli.api.artifacts.utils import symlink_or_copy
from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB, ObjectType
from snowflake.cli.api.identifiers import FQN
from snowflake.cli.api.project.schemas.entities.common import Artifacts
from snowflake.cli.api.secure_path import SecurePath
from snowflake.cli.api.sql_execution import SqlExecutionMixin
from snowflake.connector.cursor import DictCursor, SnowflakeCursor
Expand Down Expand Up @@ -95,6 +105,109 @@ def create(
except ProgrammingError as e:
handle_object_already_exists(e, ObjectType.SERVICE, service_name)

def deploy(
self,
service: ServiceEntityModel,
service_project_paths: ServiceProjectPaths,
replace: bool,
) -> SnowflakeCursor:
service_fqn = service.fqn

# SPCS service doesn't support replace in create query, so we need to drop the service first.
if replace:
object_manager = ObjectManager()
object_type = ObjectType.SERVICE.value.cli_name
if object_manager.object_exists(object_type=object_type, fqn=service_fqn):
object_manager.drop(object_type=object_type, fqn=service_fqn)

# create stage
stage_manager = StageManager()
stage_manager.create(fqn=FQN.from_stage(service.stage))

stage = stage_manager.get_standard_stage_prefix(service.stage)
self._upload_artifacts(
stage_manager=stage_manager,
service_project_paths=service_project_paths,
artifacts=service.artifacts,
stage=stage,
)

# create service
query = [
f"CREATE SERVICE {service_fqn}",
f"IN COMPUTE POOL {service.compute_pool}",
f"FROM {stage}",
f"SPECIFICATION_FILE = '{service.spec_file}'",
]

if service.min_instances:
query.append(f"MIN_INSTANCES = {service.min_instances}")

if service.max_instances:
query.append(f"MAX_INSTANCES = {service.max_instances}")

if service.query_warehouse:
query.append(f"QUERY_WAREHOUSE = {service.query_warehouse}")

if service.comment:
query.append(f"COMMENT = '{service.comment}'")

if service.external_access_integrations:
external_access_integration_list = ",".join(
f"{e}" for e in service.external_access_integrations
)
query.append(
f"EXTERNAL_ACCESS_INTEGRATIONS = ({external_access_integration_list})"
)

if service.tags:
tag_list = ",".join(
f"{t.name}={t.value_string_literal()}" for t in service.tags
)
query.append(f"WITH TAG ({tag_list})")

try:
return self.execute_query(strip_empty_lines(query))
except ProgrammingError as e:
handle_object_already_exists(e, ObjectType.SERVICE, service_fqn.identifier)

@staticmethod
def _upload_artifacts(
stage_manager: StageManager,
service_project_paths: ServiceProjectPaths,
artifacts: Artifacts,
stage: str,
):
if not artifacts:
raise ValueError("Service needs to have artifacts to deploy")

bundle_map = BundleMap(
project_root=service_project_paths.project_root,
deploy_root=service_project_paths.bundle_root,
)
for artifact in artifacts:
bundle_map.add(artifact)

service_project_paths.remove_up_bundle_root()
for (absolute_src, absolute_dest) in bundle_map.all_mappings(
absolute=True, expand_directories=True
):
# We treat the bundle/service root as deploy root
symlink_or_copy(
absolute_src,
absolute_dest,
deploy_root=service_project_paths.bundle_root,
)
stage_path = (
PurePosixPath(absolute_dest)
.relative_to(service_project_paths.bundle_root)
.parent
)
full_stage_path = f"{stage}/{stage_path}".rstrip("/")
stage_manager.put(
local_path=absolute_dest, stage_path=full_stage_path, overwrite=True
)

def execute_job(
self,
job_service_name: str,
Expand Down
6 changes: 6 additions & 0 deletions src/snowflake/cli/_plugins/spcs/services/service_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from snowflake.cli._plugins.spcs.services.service_entity_model import ServiceEntityModel
from snowflake.cli.api.entities.common import EntityBase


class ServiceEntity(EntityBase[ServiceEntityModel]):
pass
33 changes: 33 additions & 0 deletions src/snowflake/cli/_plugins/spcs/services/service_entity_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from pathlib import Path
from typing import List, Literal, Optional

from pydantic import Field
from snowflake.cli._plugins.object.common import Tag
from snowflake.cli.api.project.schemas.entities.common import (
EntityModelBaseWithArtifacts,
ExternalAccessBaseModel,
)
from snowflake.cli.api.project.schemas.updatable_model import DiscriminatorField


class ServiceEntityModel(EntityModelBaseWithArtifacts, ExternalAccessBaseModel):
type: Literal["service"] = DiscriminatorField() # noqa: A003
stage: str = Field(
title="Stage where the service specification file is located", default=None
)
compute_pool: str = Field(title="Compute pool to run the service on", default=None)
spec_file: Path = Field(
title="Path to service specification file on stage", default=None
)
min_instances: Optional[int] = Field(
title="Minimum number of instances", default=None, ge=0
)
max_instances: Optional[int] = Field(
title="Maximum number of instances", default=None
)
query_warehouse: Optional[str] = Field(
title="Warehouse to use if a service container connects to Snowflake to execute a query without explicitly specifying a warehouse to use",
default=None,
)
tags: Optional[List[Tag]] = Field(title="Tag for the service", default=None)
comment: Optional[str] = Field(title="Comment for the service", default=None)
15 changes: 15 additions & 0 deletions src/snowflake/cli/_plugins/spcs/services/service_project_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from dataclasses import dataclass
from pathlib import Path

from snowflake.cli.api.project.project_paths import ProjectPaths, bundle_root


@dataclass
class ServiceProjectPaths(ProjectPaths):
"""
This class allows you to manage files paths related to given project.
"""

@property
def bundle_root(self) -> Path:
return bundle_root(self.project_root, "service")
4 changes: 4 additions & 0 deletions src/snowflake/cli/api/project/schemas/entities/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
from snowflake.cli._plugins.spcs.image_repository.image_repository_entity_model import (
ImageRepositoryEntityModel,
)
from snowflake.cli._plugins.spcs.services.service_entity import ServiceEntity
from snowflake.cli._plugins.spcs.services.service_entity_model import ServiceEntityModel
from snowflake.cli._plugins.streamlit.streamlit_entity import StreamlitEntity
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
StreamlitEntityModel,
Expand All @@ -57,6 +59,7 @@
FunctionEntity,
ComputePoolEntity,
ImageRepositoryEntity,
ServiceEntity,
]
EntityModel = Union[
ApplicationEntityModel,
Expand All @@ -66,6 +69,7 @@
ProcedureEntityModel,
ComputePoolEntityModel,
ImageRepositoryEntityModel,
ServiceEntityModel,
]

ALL_ENTITIES: List[Entity] = [*get_args(Entity)]
Expand Down
Loading

0 comments on commit d2fe300

Please sign in to comment.