Skip to content

Commit 9a3a517

Browse files
authored
airbyte-ci: introduce ConnectorTestContext (#38628)
1 parent 503b819 commit 9a3a517

File tree

11 files changed

+207
-184
lines changed

11 files changed

+207
-184
lines changed

.github/workflows/publish_connectors.yml

+1-3
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ on:
1616
default: "--pre-release"
1717
airbyte_ci_binary_url:
1818
description: "URL to the airbyte-ci binary to use for the action. If not provided, the action will use the latest release of airbyte-ci."
19-
# Pinning to 4.15.0 as 4.15.1 has a bug:
20-
# https://github.com/airbytehq/airbyte/actions/runs/9211856191/job/25342184646
21-
default: "https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/4.15.0/airbyte-ci"
19+
default: "https://connectors.airbyte.com/airbyte-ci/releases/ubuntu/latest/airbyte-ci"
2220
jobs:
2321
publish_connectors:
2422
name: Publish connectors

airbyte-ci/connectors/pipelines/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,8 @@ E.G.: running Poe tasks on the modified internal packages of the current branch:
748748

749749
| Version | PR | Description |
750750
| ------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
751-
| 4.15.1 | [#38615](https://github.com/airbytehq/airbyte/pull/38615) | Do not eagerly fetch connector secrets. |
751+
| 4.15.2 | [#38628](https://github.com/airbytehq/airbyte/pull/38628) | Introduce ConnectorTestContext to avoid trying fetching connector secret in the PublishContext. |
752+
| 4.15.1 | [#38615](https://github.com/airbytehq/airbyte/pull/38615) | Do not eagerly fetch connector secrets. |
752753
| 4.15.0 | [#38322](https://github.com/airbytehq/airbyte/pull/38322) | Introduce a SecretStore abstraction to fetch connector secrets from metadata files. |
753754
| 4.14.1 | [#38582](https://github.com/airbytehq/airbyte/pull/38582) | Fixed bugs in `up_to_date` flags, `pull_request` version change logic. |
754755
| 4.14.0 | [#38281](https://github.com/airbytehq/airbyte/pull/38281) | Conditionally run test suites according to `connectorTestSuitesOptions` in metadata files. |

airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/context.py

+2-158
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from __future__ import annotations
88

9-
from copy import deepcopy
109
from datetime import datetime
1110
from pathlib import Path
1211
from types import TracebackType
@@ -16,7 +15,6 @@
1615
from asyncer import asyncify
1716
from dagger import Directory, Platform
1817
from github import PullRequest
19-
from pipelines.airbyte_ci.connectors.consts import CONNECTOR_TEST_STEP_ID
2018
from pipelines.airbyte_ci.connectors.reports import ConnectorReport
2119
from pipelines.consts import BUILD_PLATFORMS
2220
from pipelines.dagger.actions import secrets
@@ -26,19 +24,11 @@
2624
from pipelines.helpers.slack import send_message_to_webhook
2725
from pipelines.helpers.utils import METADATA_FILE_NAME
2826
from pipelines.models.contexts.pipeline_context import PipelineContext
29-
from pipelines.models.secrets import LocalDirectorySecretStore, Secret, SecretNotFoundError, SecretStore
30-
from pydash import find # type: ignore
27+
from pipelines.models.secrets import LocalDirectorySecretStore, Secret, SecretStore
3128

3229
if TYPE_CHECKING:
33-
from logging import Logger
3430
from pathlib import Path as NativePath
3531
from typing import Dict, FrozenSet, List, Optional, Sequence
36-
# These test suite names are declared in metadata.yaml files
37-
TEST_SUITE_NAME_TO_STEP_ID = {
38-
"unitTests": CONNECTOR_TEST_STEP_ID.UNIT,
39-
"integrationTests": CONNECTOR_TEST_STEP_ID.INTEGRATION,
40-
"acceptanceTests": CONNECTOR_TEST_STEP_ID.ACCEPTANCE,
41-
}
4232

4333

4434
class ConnectorContext(PipelineContext):
@@ -147,11 +137,10 @@ def __init__(
147137
ci_gcp_credentials=ci_gcp_credentials,
148138
ci_git_user=ci_git_user,
149139
ci_github_access_token=ci_github_access_token,
150-
run_step_options=self._skip_metadata_disabled_test_suites(run_step_options),
140+
run_step_options=run_step_options,
151141
enable_report_auto_open=enable_report_auto_open,
152142
secret_stores=secret_stores,
153143
)
154-
self.step_id_to_secrets_mapping = self._get_step_id_to_secret_mapping()
155144

156145
@property
157146
def modified_files(self) -> FrozenSet[NativePath]:
@@ -233,124 +222,6 @@ async def get_connector_dir(self, exclude: Optional[List[str]] = None, include:
233222
vanilla_connector_dir = self.get_repo_dir(str(self.connector.code_directory), exclude=exclude, include=include)
234223
return await vanilla_connector_dir.with_timestamps(1)
235224

236-
@staticmethod
237-
def _handle_missing_secret_store(
238-
secret_info: Dict[str, str | Dict[str, str]], raise_on_missing: bool, logger: Optional[Logger] = None
239-
) -> None:
240-
assert isinstance(secret_info["secretStore"], dict), "The secretStore field must be a dict"
241-
message = f"Secret {secret_info['name']} can't be retrieved as {secret_info['secretStore']['alias']} is not available"
242-
if raise_on_missing:
243-
raise SecretNotFoundError(message)
244-
if logger is not None:
245-
logger.warn(message)
246-
247-
@staticmethod
248-
def _process_secret(
249-
secret_info: Dict[str, str | Dict[str, str]],
250-
secret_stores: Dict[str, SecretStore],
251-
raise_on_missing: bool,
252-
logger: Optional[Logger] = None,
253-
) -> Optional[Secret]:
254-
assert isinstance(secret_info["secretStore"], dict), "The secretStore field must be a dict"
255-
secret_store_alias = secret_info["secretStore"]["alias"]
256-
if secret_store_alias not in secret_stores:
257-
ConnectorContext._handle_missing_secret_store(secret_info, raise_on_missing, logger)
258-
return None
259-
else:
260-
# All these asserts and casting are there to make MyPy happy
261-
# The dict structure being nested MyPy can't figure if the values are str or dict
262-
assert isinstance(secret_info["name"], str), "The secret name field must be a string"
263-
if file_name := secret_info.get("fileName"):
264-
assert isinstance(secret_info["fileName"], str), "The secret fileName must be a string"
265-
file_name = str(secret_info["fileName"])
266-
else:
267-
file_name = None
268-
return Secret(secret_info["name"], secret_stores[secret_store_alias], file_name=file_name)
269-
270-
@staticmethod
271-
def get_secrets_from_connector_test_suites_option(
272-
connector_test_suites_options: List[Dict[str, str | Dict[str, List[Dict[str, str | Dict[str, str]]]]]],
273-
suite_name: str,
274-
secret_stores: Dict[str, SecretStore],
275-
raise_on_missing_secret_store: bool = True,
276-
logger: Logger | None = None,
277-
) -> List[Secret]:
278-
"""Get secrets declared in metadata connectorTestSuitesOptions for a test suite name.
279-
It will use the secret store alias declared in connectorTestSuitesOptions.
280-
If the secret store is not available a warning or and error could be raised according to the raise_on_missing_secret_store parameter value.
281-
We usually want to raise an error when running in CI context and log a warning when running locally, as locally we can fallback on local secrets.
282-
283-
Args:
284-
connector_test_suites_options (List[Dict[str, str | Dict]]): The connector under test test suite options
285-
suite_name (str): The test suite name
286-
secret_stores (Dict[str, SecretStore]): The available secrets stores
287-
raise_on_missing_secret_store (bool, optional): Raise an error if the secret store declared in the connectorTestSuitesOptions is not available. Defaults to True.
288-
logger (Logger | None, optional): Logger to log a warning if the secret store declared in the connectorTestSuitesOptions is not available. Defaults to None.
289-
290-
Raises:
291-
SecretNotFoundError: Raised if the secret store declared in the connectorTestSuitesOptions is not available and raise_on_missing_secret_store is truthy.
292-
293-
Returns:
294-
List[Secret]: List of secrets declared in the connectorTestSuitesOptions for a test suite name.
295-
"""
296-
secrets: List[Secret] = []
297-
enabled_test_suite = find(connector_test_suites_options, lambda x: x["suite"] == suite_name)
298-
299-
if enabled_test_suite and "testSecrets" in enabled_test_suite:
300-
for secret_info in enabled_test_suite["testSecrets"]:
301-
if secret := ConnectorContext._process_secret(secret_info, secret_stores, raise_on_missing_secret_store, logger):
302-
secrets.append(secret)
303-
304-
return secrets
305-
306-
def get_connector_secrets_for_test_suite(
307-
self, test_suite_name: str, connector_test_suites_options: List, local_secrets: List[Secret]
308-
) -> List[Secret]:
309-
"""Get secrets to use for a test suite.
310-
Always merge secrets declared in metadata's connectorTestSuiteOptions with secrets declared locally.
311-
312-
Args:
313-
test_suite_name (str): Name of the test suite to get secrets for
314-
context (ConnectorContext): The current connector context
315-
connector_test_suites_options (Dict): The current connector test suite options (from metadata)
316-
local_secrets (List[Secret]): The local connector secrets.
317-
318-
Returns:
319-
List[Secret]: Secrets to use to run the passed test suite name.
320-
"""
321-
return (
322-
self.get_secrets_from_connector_test_suites_option(
323-
connector_test_suites_options,
324-
test_suite_name,
325-
self.secret_stores,
326-
raise_on_missing_secret_store=self.is_ci,
327-
logger=self.logger,
328-
)
329-
+ local_secrets
330-
)
331-
332-
def _get_step_id_to_secret_mapping(self) -> Dict[CONNECTOR_TEST_STEP_ID, List[Secret]]:
333-
step_id_to_secrets: Dict[CONNECTOR_TEST_STEP_ID, List[Secret]] = {
334-
CONNECTOR_TEST_STEP_ID.UNIT: [],
335-
CONNECTOR_TEST_STEP_ID.INTEGRATION: [],
336-
CONNECTOR_TEST_STEP_ID.ACCEPTANCE: [],
337-
}
338-
local_secrets = self.local_secret_store.get_all_secrets() if self.local_secret_store else []
339-
connector_test_suites_options = self.metadata.get("connectorTestSuitesOptions", [])
340-
341-
keep_steps = set(self.run_step_options.keep_steps or [])
342-
skip_steps = set(self.run_step_options.skip_steps or [])
343-
344-
for test_suite_name, step_id in TEST_SUITE_NAME_TO_STEP_ID.items():
345-
if step_id in keep_steps or (not keep_steps and step_id not in skip_steps):
346-
step_id_to_secrets[step_id] = self.get_connector_secrets_for_test_suite(
347-
test_suite_name, connector_test_suites_options, local_secrets
348-
)
349-
return step_id_to_secrets
350-
351-
def get_secrets_for_step_id(self, step_id: CONNECTOR_TEST_STEP_ID) -> List[Secret]:
352-
return self.step_id_to_secrets_mapping.get(step_id, [])
353-
354225
async def __aexit__(
355226
self, exception_type: Optional[type[BaseException]], exception_value: Optional[BaseException], traceback: Optional[TracebackType]
356227
) -> bool:
@@ -395,30 +266,3 @@ async def __aexit__(
395266

396267
def create_slack_message(self) -> str:
397268
raise NotImplementedError
398-
399-
def _get_step_id_to_skip_according_to_metadata(self) -> List[CONNECTOR_TEST_STEP_ID]:
400-
"""The connector metadata have a connectorTestSuitesOptions field.
401-
It allows connector developers to declare the test suites that are enabled for a connector.
402-
This function retrieved enabled test suites according to this field value and returns the test suites steps that are skipped (because they're not declared in this field.)
403-
The skippable test suites steps are declared in TEST_SUITE_NAME_TO_STEP_ID.
404-
405-
Returns:
406-
List[CONNECTOR_TEST_STEP_ID]: List of step ids that should be skipped according to connector metadata.
407-
"""
408-
enabled_test_suites = [option["suite"] for option in self.metadata.get("connectorTestSuitesOptions", [])]
409-
return [step_id for test_suite_name, step_id in TEST_SUITE_NAME_TO_STEP_ID.items() if test_suite_name not in enabled_test_suites]
410-
411-
def _skip_metadata_disabled_test_suites(self, run_step_options: RunStepOptions) -> RunStepOptions:
412-
"""Updated the original run_step_options to skip the disabled test suites according to connector metadata.
413-
414-
Args:
415-
run_step_options (RunStepOptions): Original run step options.
416-
417-
Returns:
418-
RunStepOptions: Updated run step options.
419-
"""
420-
run_step_options = deepcopy(run_step_options)
421-
# If any `skip_steps` are present, we will run everything except the skipped steps, instead of just `keep_steps`.
422-
if not run_step_options.keep_steps:
423-
run_step_options.skip_steps += self._get_step_id_to_skip_according_to_metadata()
424-
return run_step_options

airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/pipeline.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77

88
import sys
99
from pathlib import Path
10-
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union
10+
from typing import TYPE_CHECKING, Any, Callable, List, Optional
1111

1212
import anyio
1313
import dagger
1414
from connector_ops.utils import ConnectorLanguage # type: ignore
1515
from dagger import Config
1616
from pipelines.airbyte_ci.connectors.context import ConnectorContext
1717
from pipelines.airbyte_ci.connectors.publish.context import PublishConnectorContext
18+
from pipelines.airbyte_ci.connectors.test.context import ConnectorTestContext
1819
from pipelines.airbyte_ci.steps.no_op import NoOpStep
1920
from pipelines.consts import ContextState
2021
from pipelines.dagger.actions.system import docker
@@ -51,7 +52,8 @@ async def context_to_step_result(context: PipelineContext) -> StepResult:
5152
# HACK: This is to avoid wrapping the whole pipeline in a dagger pipeline to avoid instability just prior to launch
5253
# TODO (ben): Refactor run_connectors_pipelines to wrap the whole pipeline in a dagger pipeline once Steps are refactored
5354
async def run_report_complete_pipeline(
54-
dagger_client: dagger.Client, contexts: List[ConnectorContext] | List[PublishConnectorContext] | List[PipelineContext]
55+
dagger_client: dagger.Client,
56+
contexts: List[ConnectorContext] | List[PublishConnectorContext] | List[PipelineContext] | List[ConnectorTestContext],
5557
) -> None:
5658
"""Create and Save a report representing the run of the encompassing pipeline.
5759
@@ -81,14 +83,14 @@ async def run_report_complete_pipeline(
8183

8284

8385
async def run_connectors_pipelines(
84-
contexts: Union[List[ConnectorContext], List[PublishConnectorContext]],
86+
contexts: List[ConnectorContext] | List[PublishConnectorContext] | List[ConnectorTestContext],
8587
connector_pipeline: Callable,
8688
pipeline_name: str,
8789
concurrency: int,
8890
dagger_logs_path: Optional[Path],
8991
execute_timeout: Optional[int],
9092
*args: Any,
91-
) -> List[ConnectorContext] | List[PublishConnectorContext]:
93+
) -> List[ConnectorContext] | List[PublishConnectorContext] | List[ConnectorTestContext]:
9294
"""Run a connector pipeline for all the connector contexts."""
9395

9496
default_connectors_semaphore = anyio.Semaphore(concurrency)

airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/commands.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import asyncclick as click
99
from pipelines import main_logger
1010
from pipelines.airbyte_ci.connectors.consts import CONNECTOR_TEST_STEP_ID
11-
from pipelines.airbyte_ci.connectors.context import ConnectorContext
1211
from pipelines.airbyte_ci.connectors.pipeline import run_connectors_pipelines
12+
from pipelines.airbyte_ci.connectors.test.context import ConnectorTestContext
1313
from pipelines.airbyte_ci.connectors.test.pipeline import run_connector_test_pipeline
1414
from pipelines.airbyte_ci.connectors.test.steps.common import RegressionTests
1515
from pipelines.cli.click_decorators import click_ci_requirements_option
@@ -134,7 +134,7 @@ async def test(
134134
)
135135

136136
connectors_tests_contexts = [
137-
ConnectorContext(
137+
ConnectorTestContext(
138138
pipeline_name=f"{global_status_check_context} on {connector.technical_name}",
139139
connector=connector,
140140
is_local=ctx.obj["is_local"],

0 commit comments

Comments
 (0)