Skip to content

Commit 9fae538

Browse files
shalev007Shalev AvharTankilevitch
authored
[Core] Add support for choosing default resources integration will create dynamically (#1129)
# Description What - Add the option to create custom type of resources for an integration Why - Ocean Saas is the only type of Ocean integrations that can use secrets How - added a new config parameter to the integration settings ## Type of change Please leave one option from the following and delete the rest: - [ ] Bug fix (non-breaking change which fixes an issue) - [X] New feature (non-breaking change which adds functionality) - [ ] New Integration (non-breaking change which adds a new integration) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Non-breaking change (fix of existing functionality that will not change current behavior) - [ ] Documentation (added/updated documentation) <h4> All tests should be run against the port production environment(using a testing org). </h4> ### Core testing checklist - [x] Integration able to create all default resources from scratch - [x] Resync finishes successfully - [x] Resync able to create entities - [x] Resync able to update entities - [x] Resync able to detect and delete entities - [ ] Scheduled resync able to abort existing resync and start a new one - [ ] Tested with at least 2 integrations from scratch - [ ] Tested with Kafka and Polling event listeners - [ ] Tested deletion of entities that don't pass the selector ### Integration testing checklist - [ ] Integration able to create all default resources from scratch - [ ] Resync able to create entities - [ ] Resync able to update entities - [ ] Resync able to detect and delete entities - [ ] Resync finishes successfully - [ ] If new resource kind is added or updated in the integration, add example raw data, mapping and expected result to the `examples` folder in the integration directory. - [ ] If resource kind is updated, run the integration with the example data and check if the expected result is achieved - [ ] If new resource kind is added or updated, validate that live-events for that resource are working as expected - [ ] Docs PR link [here](#) ### Preflight checklist - [ ] Handled rate limiting - [ ] Handled pagination - [ ] Implemented the code in async - [ ] Support Multi account ## Screenshots Include screenshots from your environment showing how the resources of the integration will look. ## API Documentation Provide links to the API documentation used for this integration. --------- Co-authored-by: Shalev Avhar <shalev@getport.io> Co-authored-by: Tom Tankilevitch <59158507+Tankilevitch@users.noreply.github.com>
1 parent 9e6591b commit 9fae538

File tree

8 files changed

+215
-14
lines changed

8 files changed

+215
-14
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
<!-- towncrier release notes start -->
99

10+
## 0.14.0 (2024-11-12)
11+
12+
13+
### Improvements
14+
15+
- Add support for choosing default resources that the integration will create dynamically
16+
1017
## 0.13.1 (2024-11-12)
1118

1219

port_ocean/cli/commands/defaults/clean.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,6 @@ def clean(path: str, force: bool, wait: bool) -> None:
5151
default_app,
5252
)
5353

54-
clean_defaults(app.integration.AppConfigHandlerClass.CONFIG_CLASS, force, wait)
54+
clean_defaults(
55+
app.integration.AppConfigHandlerClass.CONFIG_CLASS, app.config, force, wait
56+
)

port_ocean/config/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow):
7878
default_factory=lambda: IntegrationSettings(type="", identifier="")
7979
)
8080
runtime: Runtime = Runtime.OnPrem
81+
resources_path: str = Field(default=".port/resources")
8182

8283
@root_validator()
8384
def validate_integration_config(cls, values: dict[str, Any]) -> dict[str, Any]:

port_ocean/core/defaults/clean.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import httpx
55
from loguru import logger
66

7+
from port_ocean.config.settings import IntegrationConfiguration
78
from port_ocean.context.ocean import ocean
89
from port_ocean.core.defaults.common import (
910
get_port_integration_defaults,
@@ -14,26 +15,32 @@
1415

1516
def clean_defaults(
1617
config_class: Type[PortAppConfig],
18+
integration_config: IntegrationConfiguration,
1719
force: bool,
1820
wait: bool,
1921
) -> None:
2022
try:
2123
asyncio.new_event_loop().run_until_complete(
22-
_clean_defaults(config_class, force, wait)
24+
_clean_defaults(config_class, integration_config, force, wait)
2325
)
2426

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

2830

2931
async def _clean_defaults(
30-
config_class: Type[PortAppConfig], force: bool, wait: bool
32+
config_class: Type[PortAppConfig],
33+
integration_config: IntegrationConfiguration,
34+
force: bool,
35+
wait: bool,
3136
) -> None:
3237
port_client = ocean.port_client
3338
is_exists = await is_integration_exists(port_client)
3439
if not is_exists:
3540
return None
36-
defaults = get_port_integration_defaults(config_class)
41+
defaults = get_port_integration_defaults(
42+
config_class, integration_config.resources_path
43+
)
3744
if not defaults:
3845
return None
3946

port_ocean/core/defaults/common.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Type, Any, TypedDict, Optional
44

55
import httpx
6+
from loguru import logger
67
import yaml
78
from pydantic import BaseModel, Field
89
from starlette import status
@@ -77,18 +78,33 @@ def deconstruct_blueprints_to_creation_steps(
7778
)
7879

7980

81+
def is_valid_dir(path: Path) -> bool:
82+
return path.is_dir()
83+
84+
8085
def get_port_integration_defaults(
81-
port_app_config_class: Type[PortAppConfig], base_path: Path = Path(".")
86+
port_app_config_class: Type[PortAppConfig],
87+
custom_defaults_dir: Optional[str] = None,
88+
base_path: Path = Path("."),
8289
) -> Defaults | None:
83-
defaults_dir = base_path / ".port/resources"
84-
if not defaults_dir.exists():
85-
return None
86-
87-
if not defaults_dir.is_dir():
88-
raise UnsupportedDefaultFileType(
89-
f"Defaults directory is not a directory: {defaults_dir}"
90+
fallback_dir = base_path / ".port/resources"
91+
92+
if custom_defaults_dir and is_valid_dir(base_path / custom_defaults_dir):
93+
defaults_dir = base_path / custom_defaults_dir
94+
elif is_valid_dir(fallback_dir):
95+
logger.info(
96+
f"Could not find custom defaults directory {custom_defaults_dir}, falling back to {fallback_dir}",
97+
fallback_dir=fallback_dir,
98+
custom_defaults_dir=custom_defaults_dir,
99+
)
100+
defaults_dir = fallback_dir
101+
else:
102+
logger.warning(
103+
f"Could not find defaults directory {fallback_dir}, skipping defaults"
90104
)
105+
return None
91106

107+
logger.info(f"Loading defaults from {defaults_dir}", defaults_dir=defaults_dir)
92108
default_jsons = {}
93109
allowed_file_names = [
94110
field_model.alias for _, field_model in Defaults.__fields__.items()

port_ocean/core/defaults/initialize.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,9 @@ async def _initialize_defaults(
198198
config_class: Type[PortAppConfig], integration_config: IntegrationConfiguration
199199
) -> None:
200200
port_client = ocean.port_client
201-
defaults = get_port_integration_defaults(config_class)
201+
defaults = get_port_integration_defaults(
202+
config_class, integration_config.resources_path
203+
)
202204
if not defaults:
203205
logger.warning("No defaults found. Skipping initialization...")
204206
return None
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import pytest
2+
import json
3+
from unittest.mock import patch
4+
from pathlib import Path
5+
from port_ocean.core.handlers.port_app_config.models import PortAppConfig
6+
from port_ocean.core.defaults.common import (
7+
get_port_integration_defaults,
8+
Defaults,
9+
)
10+
11+
12+
@pytest.fixture
13+
def setup_mock_directories(tmp_path: Path) -> tuple[Path, Path, Path]:
14+
# Create .port/resources with sample files
15+
default_dir = tmp_path / ".port/resources"
16+
default_dir.mkdir(parents=True, exist_ok=True)
17+
18+
# Create mock JSON and YAML files with expected content
19+
(default_dir / "blueprints.json").write_text(
20+
json.dumps(
21+
[
22+
{
23+
"identifier": "mock-identifier",
24+
"title": "mock-title",
25+
"icon": "mock-icon",
26+
"schema": {
27+
"type": "object",
28+
"properties": {"key": {"type": "string"}},
29+
},
30+
}
31+
]
32+
)
33+
)
34+
(default_dir / "port-app-config.json").write_text(
35+
json.dumps(
36+
{
37+
"resources": [
38+
{
39+
"kind": "mock-kind",
40+
"selector": {"query": "true"},
41+
"port": {
42+
"entity": {
43+
"mappings": {
44+
"identifier": ".id",
45+
"title": ".title",
46+
"blueprint": '"mock-identifier"',
47+
}
48+
}
49+
},
50+
}
51+
]
52+
}
53+
)
54+
)
55+
56+
# Create .port/custom_resources with different sample files
57+
custom_resources_dir = tmp_path / ".port/custom_resources"
58+
custom_resources_dir.mkdir(parents=True, exist_ok=True)
59+
60+
# Create mock JSON and YAML files with expected content
61+
(custom_resources_dir / "blueprints.json").write_text(
62+
json.dumps(
63+
[
64+
{
65+
"identifier": "mock-custom-identifier",
66+
"title": "mock-custom-title",
67+
"icon": "mock-custom-icon",
68+
"schema": {
69+
"type": "object",
70+
"properties": {"key": {"type": "string"}},
71+
},
72+
}
73+
]
74+
)
75+
)
76+
(custom_resources_dir / "port-app-config.json").write_text(
77+
json.dumps(
78+
{
79+
"resources": [
80+
{
81+
"kind": "mock-custom-kind",
82+
"selector": {"query": "true"},
83+
"port": {
84+
"entity": {
85+
"mappings": {
86+
"identifier": ".id",
87+
"title": ".title",
88+
"blueprint": '"mock-custom-identifier"',
89+
}
90+
}
91+
},
92+
}
93+
]
94+
}
95+
)
96+
)
97+
98+
# Define the non-existing directory path
99+
non_existing_dir = tmp_path / ".port/do_not_exist"
100+
101+
return default_dir, custom_resources_dir, non_existing_dir
102+
103+
104+
def test_custom_defaults_dir_used_if_valid(
105+
setup_mock_directories: tuple[Path, Path, Path]
106+
) -> None:
107+
# Arrange
108+
_, custom_resources_dir, _ = setup_mock_directories
109+
110+
with (
111+
patch("port_ocean.core.defaults.common.is_valid_dir") as mock_is_valid_dir,
112+
patch(
113+
"pathlib.Path.iterdir",
114+
return_value=custom_resources_dir.iterdir(),
115+
),
116+
):
117+
mock_is_valid_dir.side_effect = lambda path: path == custom_resources_dir
118+
119+
# Act
120+
defaults = get_port_integration_defaults(
121+
port_app_config_class=PortAppConfig,
122+
custom_defaults_dir=".port/custom_resources",
123+
base_path=custom_resources_dir.parent.parent,
124+
)
125+
126+
# Assert
127+
assert isinstance(defaults, Defaults)
128+
assert defaults.blueprints[0].get("identifier") == "mock-custom-identifier"
129+
assert defaults.port_app_config is not None
130+
assert defaults.port_app_config.resources[0].kind == "mock-custom-kind"
131+
132+
133+
def test_fallback_to_default_dir_if_custom_dir_invalid(
134+
setup_mock_directories: tuple[Path, Path, Path]
135+
) -> None:
136+
resources_dir, _, non_existing_dir = setup_mock_directories
137+
138+
# Arrange
139+
with (
140+
patch("port_ocean.core.defaults.common.is_valid_dir") as mock_is_valid_dir,
141+
patch("pathlib.Path.iterdir", return_value=resources_dir.iterdir()),
142+
):
143+
144+
mock_is_valid_dir.side_effect = lambda path: path == resources_dir
145+
146+
# Act
147+
custom_defaults_dir = str(non_existing_dir.relative_to(resources_dir.parent))
148+
defaults = get_port_integration_defaults(
149+
port_app_config_class=PortAppConfig,
150+
custom_defaults_dir=custom_defaults_dir,
151+
base_path=resources_dir.parent.parent,
152+
)
153+
154+
# Assert
155+
assert isinstance(defaults, Defaults)
156+
assert defaults.blueprints[0].get("identifier") == "mock-identifier"
157+
assert defaults.port_app_config is not None
158+
assert defaults.port_app_config.resources[0].kind == "mock-kind"
159+
160+
161+
def test_default_resources_path_does_not_exist() -> None:
162+
# Act
163+
defaults = get_port_integration_defaults(port_app_config_class=PortAppConfig)
164+
165+
# Assert
166+
assert defaults is None

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "port-ocean"
3-
version = "0.13.1"
3+
version = "0.14.0"
44
description = "Port Ocean is a CLI tool for managing your Port projects."
55
readme = "README.md"
66
homepage = "https://app.getport.io"

0 commit comments

Comments
 (0)