Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add clear and apply commands #74

Merged
merged 19 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,4 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Added `ocean version` to get the framework version.
- Added `make new` to scaffold in the Ocean repository.

(PORT-4307)
(PORT-4307)
yairsimantov20 marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 3 additions & 3 deletions integrations/pagerduty/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ class ObjectKind:


pager_duty_client = PagerDutyClient(
ocean.integration_config["token"],
ocean.integration_config["api_url"],
ocean.integration_config["app_host"],
ocean.integration_config.get("token", ""),
ocean.integration_config.get("api_url", ""),
ocean.integration_config.get("app_host", ""),
danielsinai marked this conversation as resolved.
Show resolved Hide resolved
)


Expand Down
4 changes: 4 additions & 0 deletions port_ocean/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from .pull import pull
from .sail import sail
from .version import version
from .defaults.dock import dock
from .defaults.clean import clean

__all__ = [
"cli_start",
Expand All @@ -12,4 +14,6 @@
"pull",
"sail",
"version",
"dock",
"clean",
]
7 changes: 7 additions & 0 deletions port_ocean/cli/commands/defaults/__init___.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .dock import dock
from .clean import clean

__all__ = [
"dock",
"clean",
]
54 changes: 54 additions & 0 deletions port_ocean/cli/commands/defaults/clean.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
import click

from inspect import getmembers
from .group import defaults
from port_ocean.cli.commands.main import print_logo, console
from port_ocean.ocean import Ocean
from port_ocean.cli.defaults import clean_defaults
from port_ocean.run import _create_default_app, _load_module


@defaults.command()
@click.argument("path", default=".", type=click.Path(exists=True))
@click.option(
danielsinai marked this conversation as resolved.
Show resolved Hide resolved
"-f",
"--force",
"force",
type=bool,
default=False,
help="Delete all the entities of the Blueprint as well as the blueprint itself.",
)
@click.option(
"-w",
"--wait",
"wait",
type=bool,
default=False,
help="Wait for the migration to finish. when force is set to true.",
)
def clean(path: str, force: bool, wait: bool) -> None:
"""
Clean defaults of the integration from the .port/resources PATH.

PATH: Path to the integration. If not provided, the current directory will be used.
"""
print_logo()

console.print("Cleaning blueprints and configurations! ⚓️")

if force:
console.print(
"Deleting entities forcefully I sure hope you know what you are doing 🚨 🚨 🚨 ",
yairsimantov20 marked this conversation as resolved.
Show resolved Hide resolved
)
default_app = _create_default_app(path, False)

main_path = f"{path}/main.py" if path else "main.py"

app_module = _load_module(main_path)
app: Ocean = {name: item for name, item in getmembers(app_module)}.get(
"app",
default_app,
)

clean_defaults(app.integration.AppConfigHandlerClass.CONFIG_CLASS, force, wait)
37 changes: 37 additions & 0 deletions port_ocean/cli/commands/defaults/dock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
import click

from inspect import getmembers
from .group import defaults
from port_ocean.cli.commands.main import print_logo, console
from port_ocean.ocean import Ocean
from port_ocean.cli.defaults.initialize import initialize_defaults
from port_ocean.run import _create_default_app, _load_module


@defaults.command()
@click.argument("path", default=".", type=click.Path(exists=True))
def dock(path: str) -> None:
"""
Apply defaults of the integration from the .port/resources PATH.

PATH: Path to the integration. If not provided, the current directory will be used.
"""
print_logo()

console.print("Unloading cargo at the dock... 📦🚢")

default_app = _create_default_app(path, False)

main_path = f"{path}/main.py" if path else "main.py"

app_module = _load_module(main_path)
app: Ocean = {name: item for name, item in getmembers(app_module)}.get(
"app",
default_app,
)

initialize_defaults(
app.integration.AppConfigHandlerClass.CONFIG_CLASS,
app.config,
)
6 changes: 6 additions & 0 deletions port_ocean/cli/commands/defaults/group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from port_ocean.cli.commands.main import cli_start


@cli_start.group("defaults")
def defaults() -> None:
pass
7 changes: 7 additions & 0 deletions port_ocean/cli/defaults/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .clean import clean_defaults
from .initialize import initialize_defaults

__all__ = [
"clean_defaults",
"initialize_defaults",
]
73 changes: 73 additions & 0 deletions port_ocean/cli/defaults/clean.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import asyncio
from typing import Type

import httpx
from loguru import logger

from port_ocean.context.ocean import ocean
from port_ocean.core.handlers.port_app_config.models import PortAppConfig


from port_ocean.cli.defaults.common import (
get_port_integration_defaults,
is_integration_exists,
)


def clean_defaults(
config_class: Type[PortAppConfig],
force: bool,
wait: bool,
) -> None:
try:
asyncio.new_event_loop().run_until_complete(
_clean_defaults(config_class, force, wait)
)

except Exception as e:
logger.debug(f"Failed to clear defaults, skipping... Error: {e}")
danielsinai marked this conversation as resolved.
Show resolved Hide resolved


async def _clean_defaults(
config_class: Type[PortAppConfig], force: bool, wait: bool
) -> None:
port_client = ocean.port_client
is_exists = await is_integration_exists(port_client)
if not is_exists:
return None
defaults = get_port_integration_defaults(config_class)
if not defaults:
return None

try:
migration_ids = await asyncio.gather(
*(
port_client.delete_blueprint(
blueprint["identifier"], should_raise=True, delete_entities=force
)
for blueprint in defaults.blueprints
)
)

if not force:
logger.info(
"Finished deleting blueprints and configurations! ⚓️",
)
return None

migration_ids = [migration_id for migration_id in migration_ids if migration_id]

if migration_ids and len(migration_ids) > 0 and not wait:
logger.info(
f"Migration started. To check the status of the migration, track these ids using /migrations/:id route {migration_ids}",
)
elif migration_ids and len(migration_ids) > 0 and wait:
await asyncio.gather(
*(
ocean.port_client.wait_for_migration_to_complete(migration_id)
for migration_id in migration_ids
)
)
except httpx.HTTPStatusError as e:
logger.error(f"Failed to delete blueprints: {e.response.text}.")
raise e
114 changes: 114 additions & 0 deletions port_ocean/cli/defaults/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import json
from pathlib import Path
from typing import Type, Any, TypedDict, Optional

import httpx
import yaml
from pydantic import BaseModel, Field
from starlette import status

from port_ocean.clients.port.client import PortClient
from port_ocean.core.handlers.port_app_config.models import PortAppConfig
from port_ocean.exceptions.port_defaults import (
UnsupportedDefaultFileType,
)

YAML_EXTENSIONS = [".yaml", ".yml"]
ALLOWED_FILE_TYPES = [".json", *YAML_EXTENSIONS]


class Preset(TypedDict):
blueprint: str
data: list[dict[str, Any]]


class Defaults(BaseModel):
blueprints: list[dict[str, Any]] = []
actions: list[Preset] = []
scorecards: list[Preset] = []
port_app_config: Optional[PortAppConfig] = Field(
default=None, alias="port-app-config"
)

class Config:
allow_population_by_field_name = True


async def is_integration_exists(port_client: PortClient) -> bool:
try:
await port_client.get_current_integration(should_log=False)
return True
except httpx.HTTPStatusError as e:
if e.response.status_code != status.HTTP_404_NOT_FOUND:
raise e

return False


def deconstruct_blueprints_to_creation_steps(
raw_blueprints: list[dict[str, Any]]
) -> tuple[list[dict[str, Any]], ...]:
"""
Deconstructing the blueprint into stages so the api wont fail to create a blueprint if there is a conflict
example: Preventing the failure of creating a blueprint with a relation to another blueprint
"""
(
bare_blueprint,
with_relations,
full_blueprint,
) = ([], [], [])

for blueprint in raw_blueprints.copy():
full_blueprint.append(blueprint.copy())

blueprint.pop("calculationProperties", {})
blueprint.pop("mirrorProperties", {})
with_relations.append(blueprint.copy())

blueprint.pop("teamInheritance", {})
blueprint.pop("relations", {})
bare_blueprint.append(blueprint)

return (
bare_blueprint,
with_relations,
full_blueprint,
)


def get_port_integration_defaults(
port_app_config_class: Type[PortAppConfig], base_path: Path = Path(".")
) -> Defaults | None:
defaults_dir = base_path / ".port/resources"
if not defaults_dir.exists():
return None

if not defaults_dir.is_dir():
raise UnsupportedDefaultFileType(
f"Defaults directory is not a directory: {defaults_dir}"
)

default_jsons = {}
allowed_file_names = [
field_model.alias for _, field_model in Defaults.__fields__.items()
]
for path in defaults_dir.iterdir():
if path.stem in allowed_file_names:
if not path.is_file() or path.suffix not in ALLOWED_FILE_TYPES:
raise UnsupportedDefaultFileType(
f"Defaults directory should contain only one of the next types: {ALLOWED_FILE_TYPES}. Found: {path}"
)

if path.suffix in YAML_EXTENSIONS:
default_jsons[path.stem] = yaml.safe_load(path.read_text())
else:
default_jsons[path.stem] = json.loads(path.read_text())

return Defaults(
blueprints=default_jsons.get("blueprints", []),
actions=default_jsons.get("actions", []),
scorecards=default_jsons.get("scorecards", []),
port_app_config=port_app_config_class(
**default_jsons.get("port-app-config", {})
),
)
Loading