Skip to content

Commit

Permalink
feat: prevent Writer Framework version mismatch local/cloud
Browse files Browse the repository at this point in the history
* fix: comment from review
  • Loading branch information
FabienArcellier committed Nov 21, 2024
1 parent 57cbc5d commit e5d4184
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 1,367 deletions.
1,209 changes: 0 additions & 1,209 deletions apps/default/poetry.lock

This file was deleted.

8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ websockets = ">= 12, < 13"
writer-sdk = ">= 1.2.0, < 2"
python-multipart = ">=0.0.7, < 1"
toml = "^0.10.2"
packaging = "^24.2"


[tool.poetry.group.build]
Expand Down
85 changes: 58 additions & 27 deletions src/writer/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import requests
from click import ClickException
from gitignore_parser import parse_gitignore
from packaging.version import Version

from writer import wf_project

Expand All @@ -24,15 +25,16 @@ def cloud():
"""A group of commands to deploy the app on writer cloud"""
pass


@cloud.command()
@click.option('--api-key',
default=lambda: os.environ.get("WRITER_API_KEY", None),
allow_from_autoenv=True,
show_envvar=True,
envvar='WRITER_API_KEY',
prompt="Enter your API key",
hide_input=True, help="Writer API key"
)
default=lambda: os.environ.get("WRITER_API_KEY", None),
allow_from_autoenv=True,
show_envvar=True,
envvar='WRITER_API_KEY',
prompt="Enter your API key",
hide_input=True, help="Writer API key"
)
@click.option('--env', '-e', multiple=True, default=[], help="Set environment variables for the app (e.g., --env KEY=VALUE)")
@click.option('--force', '-f', default=False, is_flag=True, help="Ignores warnings and overwrites the app")
@click.option('--verbose', '-v', default=False, is_flag=True, help="Enable verbose mode")
Expand All @@ -57,7 +59,7 @@ def deploy(path, api_key, env, verbose, force):
raise click.ClickException("The application does not have a poetry.lock file. Please run `poetry install` to generate it.")

writer_version = wf_project.writer_version(abs_path)
if wf_project.compare_semantic_version(poetry_locked_version, writer_version) == -1:
if poetry_locked_version < writer_version:
raise click.ClickException(f"locked version in poetry.lock does not match the project version use in .wf, pin a new version `poetry add writer@^{writer_version}")

env = _validate_env_vars(env)
Expand All @@ -74,6 +76,7 @@ def deploy(path, api_key, env, verbose, force):
finally:
tar.close()


def _validate_env_vars(env: Union[List[str], None]) -> Union[List[str], None]:
if env is None:
return None
Expand All @@ -84,15 +87,16 @@ def _validate_env_vars(env: Union[List[str], None]) -> Union[List[str], None]:
sys.exit(1)
return env


@cloud.command()
@click.option('--api-key',
default=lambda: os.environ.get("WRITER_API_KEY", None),
allow_from_autoenv=True,
show_envvar=True,
envvar='WRITER_API_KEY',
prompt="Enter your API key",
hide_input=True, help="Writer API key"
)
default=lambda: os.environ.get("WRITER_API_KEY", None),
allow_from_autoenv=True,
show_envvar=True,
envvar='WRITER_API_KEY',
prompt="Enter your API key",
hide_input=True, help="Writer API key"
)
@click.option('--verbose', '-v', default=False, is_flag=True, help="Enable verbose mode")
def undeploy(api_key, verbose):
"""Stop the app, app would not be available anymore."""
Expand All @@ -108,23 +112,24 @@ def undeploy(api_key, verbose):
print(e)
sys.exit(1)


@cloud.command()
@click.option('--api-key',
default=lambda: os.environ.get("WRITER_API_KEY", None),
allow_from_autoenv=True,
show_envvar=True,
envvar='WRITER_API_KEY',
prompt="Enter your API key",
hide_input=True, help="Writer API key"
)
default=lambda: os.environ.get("WRITER_API_KEY", None),
allow_from_autoenv=True,
show_envvar=True,
envvar='WRITER_API_KEY',
prompt="Enter your API key",
hide_input=True, help="Writer API key"
)
@click.option('--verbose', '-v', default=False, is_flag=True, help="Enable verbose mode")
def logs(api_key, verbose):
"""Fetch logs from the deployed app."""

deploy_url = os.getenv("WRITER_DEPLOY_URL", "https://api.writer.com/v1/deployment/apps")
sleep_interval = int(os.getenv("WRITER_DEPLOY_SLEEP_INTERVAL", '5'))

try:
try:
build_time = datetime.now(pytz.timezone('UTC')) - timedelta(days=4)
start_time = build_time
while True:
Expand Down Expand Up @@ -153,11 +158,15 @@ def logs(api_key, verbose):
except KeyboardInterrupt:
sys.exit(0)


def pack_project(path):
print(f"Creating deployment package from path: {path}")

files = []
def match(file_path) -> bool: return False

def match(file_path) -> bool:
return False

if os.path.exists(os.path.join(path, ".gitignore")):
match = parse_gitignore(os.path.join(path, ".gitignore"))
for root, dirs, filenames in os.walk(path, followlinks=False):
Expand Down Expand Up @@ -189,6 +198,7 @@ def match(file_path) -> bool: return False

return f


def check_app(deploy_url, token):
url = _get_app_url(deploy_url, token)
if url:
Expand All @@ -198,6 +208,7 @@ def check_app(deploy_url, token):
if input("[WARNING] Are you sure you want to overwrite? (y/N)").lower() != "y":
sys.exit(1)


def _get_app_url(deploy_url: str, token: str) -> Union[str, None]:
with requests.get(deploy_url, params={"lineLimit": 1}, headers={"Authorization": f"Bearer {token}"}) as resp:
try:
Expand All @@ -209,8 +220,9 @@ def _get_app_url(deploy_url: str, token: str) -> Union[str, None]:
data = resp.json()
return data['status']['url']


def get_logs(deploy_url, token, params, verbose=False):
with requests.get(deploy_url, params = params, headers={"Authorization": f"Bearer {token}"}) as resp:
with requests.get(deploy_url, params=params, headers={"Authorization": f"Bearer {token}"}) as resp:
on_error_print_and_raise(resp, verbose=verbose)
data = resp.json()

Expand All @@ -223,6 +235,7 @@ def get_logs(deploy_url, token, params, verbose=False):
logs.sort(key=lambda x: x[0])
return {"status": data["status"], "logs": logs}


def check_service_status(deploy_url, token, build_id, build_time, start_time, end_time, last_status):
data = get_logs(deploy_url, token, {
"buildId": build_id,
Expand All @@ -238,6 +251,7 @@ def check_service_status(deploy_url, token, build_id, build_time, start_time, en
print(log[0], log[1])
return status, url


def dictFromEnv(env: List[str]) -> dict:
env_dict = {}
if env is None:
Expand All @@ -258,8 +272,8 @@ def upload_package(deploy_url, tar, token, env, verbose=False, sleep_interval=5)
build_time = start_time

with requests.post(
url = deploy_url,
headers = {
url=deploy_url,
headers={
"Authorization": f"Bearer {token}",
},
files=files,
Expand All @@ -278,6 +292,8 @@ def upload_package(deploy_url, tar, token, env, verbose=False, sleep_interval=5)
time.sleep(sleep_interval)
start_time = end_time

_check_version_integrity(deploy_url)

if status == "COMPLETED":
print("Deployment successful")
print(f"URL: {url}")
Expand All @@ -293,6 +309,7 @@ def upload_package(deploy_url, tar, token, env, verbose=False, sleep_interval=5)
print(f"URL: {url}")
sys.exit(2)


def on_error_print_and_raise(resp, verbose=False):
try:
resp.raise_for_status()
Expand All @@ -301,8 +318,22 @@ def on_error_print_and_raise(resp, verbose=False):
print(resp.json())
raise e


def unauthorized_error():
print("Unauthorized. Please check your API key.")
sys.exit(1)


def _check_version_integrity(app_url: str):
"""
Check if the writer version in the deployment package is newer than the writer version running on the server.
"""
with requests.get(f"{app_url}/_meta") as resp:
data = resp.json()
if "writer_version_dev" not in data or "writer_version_run" not in data:
return

if Version(data["writer_version_dev"]) > Version(data["writer_version_run"]):
print(f"[WARNING] The writer version in the deployment package ({data['writer_version_dev']}) is newer than the writer version running on the server ({data['writer_version_run']}).")
return
10 changes: 6 additions & 4 deletions src/writer/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from fastapi.responses import FileResponse
from fastapi.routing import Mount
from fastapi.staticfiles import StaticFiles
from packaging.version import Version
from pydantic import ValidationError
from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState

Expand Down Expand Up @@ -645,16 +646,17 @@ def _warn_about_version_consistency(mode: ServeMode, app_path: str) -> None:
if dev_version is None:
return

run_version = wf_project.parse_semantic_version(VERSION)
if wf_project.compare_semantic_version(run_version, dev_version) == -1:
run_version = Version(VERSION)
if run_version < dev_version:
if mode == "run":
logger.warning(f"The version {run_version}, used to run the app, is older than the version {dev_version} used during development.")

if wf_project.use_pyproject(app_path):
logger.warning("You should update the version of writer in the pyproject.toml.")

if wf_project.use_requirement(app_path):
elif wf_project.use_requirement(app_path):
logger.warning("You should update the version of writer in the requirements.txt.")
else:
logger.warning("You should update the version of writer.")

if mode == "edit":
logger.warning(f"The version {VERSION}, used to edit the app, is older than the version {dev_version} used in previous development.")
Expand Down
18 changes: 0 additions & 18 deletions src/writer/ss_types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import dataclasses
from typing import Any, Dict, List, Optional, Protocol, Tuple, Union

from pydantic import BaseModel
Expand Down Expand Up @@ -210,22 +209,5 @@ class ComponentDefinition(TypedDict):
x: Optional[int]
y: Optional[int]


class WorkflowExecutionLog(BaseModel):
summary: List[Dict]

@dataclasses.dataclass
class SemanticVersion:
major: int
minor: int
fix: int
rc: Optional[int]

def __str__(self):
version = f"{self.major}.{self.minor}.{self.fix}"
if self.rc is not None:
version += f"rc{self.rc}"
return version

def version_tuple(self):
return (self.major, self.minor, self.fix, self.rc)
Loading

0 comments on commit e5d4184

Please sign in to comment.