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

[Core] Add support for choosing default resources integration will create dynamically #1129

Merged
Merged
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

<!-- towncrier release notes start -->

## 0.14.0 (2024-11-12)


### Improvements

- Add support for choosing default resources that the integration will create dynamically

## 0.13.1 (2024-11-12)


Expand Down
4 changes: 3 additions & 1 deletion port_ocean/cli/commands/defaults/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,6 @@ def clean(path: str, force: bool, wait: bool) -> None:
default_app,
)

clean_defaults(app.integration.AppConfigHandlerClass.CONFIG_CLASS, force, wait)
clean_defaults(
app.integration.AppConfigHandlerClass.CONFIG_CLASS, app.config, force, wait
)
1 change: 1 addition & 0 deletions port_ocean/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
default_factory=lambda: IntegrationSettings(type="", identifier="")
)
runtime: Runtime = Runtime.OnPrem
resources_path: str = Field(default=".port/resources")
Tankilevitch marked this conversation as resolved.
Show resolved Hide resolved

@root_validator()
def validate_integration_config(cls, values: dict[str, Any]) -> dict[str, Any]:
Expand Down
13 changes: 10 additions & 3 deletions port_ocean/core/defaults/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import httpx
from loguru import logger

from port_ocean.config.settings import IntegrationConfiguration
from port_ocean.context.ocean import ocean
from port_ocean.core.defaults.common import (
get_port_integration_defaults,
Expand All @@ -14,26 +15,32 @@

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

except Exception as e:
logger.error(f"Failed to clear defaults, skipping... Error: {e}")


async def _clean_defaults(
config_class: Type[PortAppConfig], force: bool, wait: bool
config_class: Type[PortAppConfig],
integration_config: IntegrationConfiguration,
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)
defaults = get_port_integration_defaults(
config_class, integration_config.resources_path
)
if not defaults:
return None

Expand Down
32 changes: 24 additions & 8 deletions port_ocean/core/defaults/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Type, Any, TypedDict, Optional

import httpx
from loguru import logger
import yaml
from pydantic import BaseModel, Field
from starlette import status
Expand Down Expand Up @@ -77,18 +78,33 @@ def deconstruct_blueprints_to_creation_steps(
)


def is_valid_dir(path: Path) -> bool:
return path.is_dir()


def get_port_integration_defaults(
port_app_config_class: Type[PortAppConfig], base_path: Path = Path(".")
port_app_config_class: Type[PortAppConfig],
custom_defaults_dir: Optional[str] = None,
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}"
fallback_dir = base_path / ".port/resources"

if custom_defaults_dir and is_valid_dir(base_path / custom_defaults_dir):
defaults_dir = base_path / custom_defaults_dir
elif is_valid_dir(fallback_dir):
logger.info(
f"Could not find custom defaults directory {custom_defaults_dir}, falling back to {fallback_dir}",
fallback_dir=fallback_dir,
custom_defaults_dir=custom_defaults_dir,
)
defaults_dir = fallback_dir
else:
logger.warning(
f"Could not find defaults directory {fallback_dir}, skipping defaults"
)
return None

logger.info(f"Loading defaults from {defaults_dir}", defaults_dir=defaults_dir)
default_jsons = {}
allowed_file_names = [
field_model.alias for _, field_model in Defaults.__fields__.items()
Expand Down
4 changes: 3 additions & 1 deletion port_ocean/core/defaults/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ async def _initialize_defaults(
config_class: Type[PortAppConfig], integration_config: IntegrationConfiguration
) -> None:
port_client = ocean.port_client
defaults = get_port_integration_defaults(config_class)
defaults = get_port_integration_defaults(
config_class, integration_config.resources_path
)
if not defaults:
logger.warning("No defaults found. Skipping initialization...")
return None
Expand Down
166 changes: 166 additions & 0 deletions port_ocean/tests/core/defaults/test_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import pytest
import json
from unittest.mock import patch
from pathlib import Path
from port_ocean.core.handlers.port_app_config.models import PortAppConfig
from port_ocean.core.defaults.common import (
get_port_integration_defaults,
Defaults,
)


@pytest.fixture
def setup_mock_directories(tmp_path: Path) -> tuple[Path, Path, Path]:
# Create .port/resources with sample files
default_dir = tmp_path / ".port/resources"
default_dir.mkdir(parents=True, exist_ok=True)

# Create mock JSON and YAML files with expected content
(default_dir / "blueprints.json").write_text(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cool, didn't know of this, #TIL worthy

json.dumps(
[
{
"identifier": "mock-identifier",
"title": "mock-title",
"icon": "mock-icon",
"schema": {
"type": "object",
"properties": {"key": {"type": "string"}},
},
}
]
)
)
(default_dir / "port-app-config.json").write_text(
json.dumps(
{
"resources": [
{
"kind": "mock-kind",
"selector": {"query": "true"},
"port": {
"entity": {
"mappings": {
"identifier": ".id",
"title": ".title",
"blueprint": '"mock-identifier"',
}
}
},
}
]
}
)
)

# Create .port/custom_resources with different sample files
custom_resources_dir = tmp_path / ".port/custom_resources"
custom_resources_dir.mkdir(parents=True, exist_ok=True)

# Create mock JSON and YAML files with expected content
(custom_resources_dir / "blueprints.json").write_text(
json.dumps(
[
{
"identifier": "mock-custom-identifier",
"title": "mock-custom-title",
"icon": "mock-custom-icon",
"schema": {
"type": "object",
"properties": {"key": {"type": "string"}},
},
}
]
)
)
(custom_resources_dir / "port-app-config.json").write_text(
json.dumps(
{
"resources": [
{
"kind": "mock-custom-kind",
"selector": {"query": "true"},
"port": {
"entity": {
"mappings": {
"identifier": ".id",
"title": ".title",
"blueprint": '"mock-custom-identifier"',
}
}
},
}
]
}
)
)

# Define the non-existing directory path
non_existing_dir = tmp_path / ".port/do_not_exist"

return default_dir, custom_resources_dir, non_existing_dir


def test_custom_defaults_dir_used_if_valid(
setup_mock_directories: tuple[Path, Path, Path]
) -> None:
# Arrange
_, custom_resources_dir, _ = setup_mock_directories

with (
patch("port_ocean.core.defaults.common.is_valid_dir") as mock_is_valid_dir,
patch(
"pathlib.Path.iterdir",
return_value=custom_resources_dir.iterdir(),
),
):
mock_is_valid_dir.side_effect = lambda path: path == custom_resources_dir

# Act
defaults = get_port_integration_defaults(
port_app_config_class=PortAppConfig,
custom_defaults_dir=".port/custom_resources",
base_path=custom_resources_dir.parent.parent,
)

# Assert
assert isinstance(defaults, Defaults)
assert defaults.blueprints[0].get("identifier") == "mock-custom-identifier"
assert defaults.port_app_config is not None
assert defaults.port_app_config.resources[0].kind == "mock-custom-kind"


def test_fallback_to_default_dir_if_custom_dir_invalid(
setup_mock_directories: tuple[Path, Path, Path]
) -> None:
resources_dir, _, non_existing_dir = setup_mock_directories

# Arrange
with (
patch("port_ocean.core.defaults.common.is_valid_dir") as mock_is_valid_dir,
patch("pathlib.Path.iterdir", return_value=resources_dir.iterdir()),
):

mock_is_valid_dir.side_effect = lambda path: path == resources_dir

# Act
custom_defaults_dir = str(non_existing_dir.relative_to(resources_dir.parent))
defaults = get_port_integration_defaults(
port_app_config_class=PortAppConfig,
custom_defaults_dir=custom_defaults_dir,
base_path=resources_dir.parent.parent,
)

# Assert
assert isinstance(defaults, Defaults)
assert defaults.blueprints[0].get("identifier") == "mock-identifier"
assert defaults.port_app_config is not None
assert defaults.port_app_config.resources[0].kind == "mock-kind"


def test_default_resources_path_does_not_exist() -> None:
# Act
defaults = get_port_integration_defaults(port_app_config_class=PortAppConfig)

# Assert
assert defaults is None
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "port-ocean"
version = "0.13.1"
version = "0.14.0"
description = "Port Ocean is a CLI tool for managing your Port projects."
readme = "README.md"
homepage = "https://app.getport.io"
Expand Down
Loading