Skip to content

Commit 311dcce

Browse files
authored
airbyte-ci: do not eagerly fetch connector secrets (#38615)
1 parent aef0fc7 commit 311dcce

File tree

9 files changed

+146
-144
lines changed

9 files changed

+146
-144
lines changed

airbyte-ci/connectors/pipelines/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ flowchart TD
305305
| `--code-tests-only` | True | False | Skip any tests not directly related to code updates. For instance, metadata checks, version bump checks, changelog verification, etc. Use this setting to help focus on code quality during development. |
306306
| `--concurrent-cat` | False | False | Make CAT tests run concurrently using pytest-xdist. Be careful about source or destination API rate limits. |
307307
| `--<step-id>.<extra-parameter>=<extra-parameter-value>` | True | | You can pass extra parameters for specific test steps. More details in the extra parameters section below |
308-
| `--ci-requirements` | False | | | Output the CI requirements as a JSON payload. It is used to determine the CI runner to use.
308+
| `--ci-requirements` | False | | | Output the CI requirements as a JSON payload. It is used to determine the CI runner to use.
309309

310310
Note:
311311

@@ -748,6 +748,7 @@ 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. |
751752
| 4.15.0 | [#38322](https://github.com/airbytehq/airbyte/pull/38322) | Introduce a SecretStore abstraction to fetch connector secrets from metadata files. |
752753
| 4.14.1 | [#38582](https://github.com/airbytehq/airbyte/pull/38582) | Fixed bugs in `up_to_date` flags, `pull_request` version change logic. |
753754
| 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

+122-3
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@
2626
from pipelines.helpers.slack import send_message_to_webhook
2727
from pipelines.helpers.utils import METADATA_FILE_NAME
2828
from pipelines.models.contexts.pipeline_context import PipelineContext
29-
from pipelines.models.secrets import LocalDirectorySecretStore, Secret, SecretStore
29+
from pipelines.models.secrets import LocalDirectorySecretStore, Secret, SecretNotFoundError, SecretStore
30+
from pydash import find # type: ignore
3031

3132
if TYPE_CHECKING:
33+
from logging import Logger
3234
from pathlib import Path as NativePath
3335
from typing import Dict, FrozenSet, List, Optional, Sequence
34-
3536
# These test suite names are declared in metadata.yaml files
3637
TEST_SUITE_NAME_TO_STEP_ID = {
3738
"unitTests": CONNECTOR_TEST_STEP_ID.UNIT,
@@ -127,7 +128,6 @@ def __init__(
127128
self.s3_build_cache_secret_key = s3_build_cache_secret_key
128129
self.concurrent_cat = concurrent_cat
129130
self.targeted_platforms = targeted_platforms
130-
131131
super().__init__(
132132
pipeline_name=pipeline_name,
133133
is_local=is_local,
@@ -151,6 +151,7 @@ def __init__(
151151
enable_report_auto_open=enable_report_auto_open,
152152
secret_stores=secret_stores,
153153
)
154+
self.step_id_to_secrets_mapping = self._get_step_id_to_secret_mapping()
154155

155156
@property
156157
def modified_files(self) -> FrozenSet[NativePath]:
@@ -232,6 +233,124 @@ async def get_connector_dir(self, exclude: Optional[List[str]] = None, include:
232233
vanilla_connector_dir = self.get_repo_dir(str(self.connector.code_directory), exclude=exclude, include=include)
233234
return await vanilla_connector_dir.with_timestamps(1)
234235

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+
235354
async def __aexit__(
236355
self, exception_type: Optional[type[BaseException]], exception_value: Optional[BaseException], traceback: Optional[TracebackType]
237356
) -> bool:

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

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from copy import deepcopy
77
from typing import TYPE_CHECKING
88

9-
import yaml # type: ignore
109
from base_images import version_registry # type: ignore
1110
from connector_ops.utils import ConnectorLanguage # type: ignore
1211
from dagger import Directory

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

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import toml
1313
from connector_ops.utils import ConnectorLanguage # type: ignore
1414
from jinja2 import Environment, PackageLoader, select_autoescape
15-
from pipelines.airbyte_ci.connectors.build_image.steps.python_connectors import BuildConnectorImages
1615
from pipelines.airbyte_ci.connectors.bump_version.pipeline import AddChangelogEntry, SetConnectorVersion, get_bumped_version
1716
from pipelines.airbyte_ci.connectors.consts import CONNECTOR_TEST_STEP_ID
1817
from pipelines.airbyte_ci.connectors.context import ConnectorContext, PipelineContext

airbyte-ci/connectors/pipelines/pipelines/airbyte_ci/connectors/test/steps/common.py

+2-101
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55
"""This module groups steps made to run tests agnostic to a connector language."""
66

77
import datetime
8-
import logging
98
import os
109
import time
1110
from abc import ABC, abstractmethod
1211
from functools import cached_property
1312
from pathlib import Path
1413
from textwrap import dedent
15-
from typing import ClassVar, Dict, List, Optional
14+
from typing import ClassVar, List, Optional
1615

1716
import requests # type: ignore
1817
import semver
@@ -26,106 +25,8 @@
2625
from pipelines.dagger.actions import secrets
2726
from pipelines.dagger.actions.python.poetry import with_poetry
2827
from pipelines.helpers.utils import METADATA_FILE_NAME, get_exec_result
29-
from pipelines.models.secrets import Secret, SecretNotFoundError, SecretStore
28+
from pipelines.models.secrets import Secret
3029
from pipelines.models.steps import STEP_PARAMS, MountPath, Step, StepResult, StepStatus
31-
from pydash import find # type: ignore
32-
33-
34-
def _handle_missing_secret_store(
35-
secret_info: Dict[str, str | Dict[str, str]], raise_on_missing: bool, logger: Optional[logging.Logger] = None
36-
) -> None:
37-
assert isinstance(secret_info["secretStore"], dict), "The secretStore field must be a dict"
38-
message = f"Secret {secret_info['name']} can't be retrieved as {secret_info['secretStore']['alias']} is not available"
39-
if raise_on_missing:
40-
raise SecretNotFoundError(message)
41-
if logger is not None:
42-
logger.warn(message)
43-
44-
45-
def _process_secret(
46-
secret_info: Dict[str, str | Dict[str, str]],
47-
secret_stores: Dict[str, SecretStore],
48-
raise_on_missing: bool,
49-
logger: Optional[logging.Logger] = None,
50-
) -> Optional[Secret]:
51-
assert isinstance(secret_info["secretStore"], dict), "The secretStore field must be a dict"
52-
secret_store_alias = secret_info["secretStore"]["alias"]
53-
if secret_store_alias not in secret_stores:
54-
_handle_missing_secret_store(secret_info, raise_on_missing, logger)
55-
return None
56-
else:
57-
# All these asserts and casting are there to make MyPy happy
58-
# The dict structure being nested MyPy can't figure if the values are str or dict
59-
assert isinstance(secret_info["name"], str), "The secret name field must be a string"
60-
if file_name := secret_info.get("fileName"):
61-
assert isinstance(secret_info["fileName"], str), "The secret fileName must be a string"
62-
file_name = str(secret_info["fileName"])
63-
else:
64-
file_name = None
65-
return Secret(secret_info["name"], secret_stores[secret_store_alias], file_name=file_name)
66-
67-
68-
def get_secrets_from_connector_test_suites_option(
69-
connector_test_suites_options: List[Dict[str, str | Dict[str, List[Dict[str, str | Dict[str, str]]]]]],
70-
suite_name: str,
71-
secret_stores: Dict[str, SecretStore],
72-
raise_on_missing_secret_store: bool = True,
73-
logger: logging.Logger | None = None,
74-
) -> List[Secret]:
75-
"""Get secrets declared in metadata connectorTestSuitesOptions for a test suite name.
76-
It will use the secret store alias declared in connectorTestSuitesOptions.
77-
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.
78-
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.
79-
80-
Args:
81-
connector_test_suites_options (List[Dict[str, str | Dict]]): The connector under test test suite options
82-
suite_name (str): The test suite name
83-
secret_stores (Dict[str, SecretStore]): The available secrets stores
84-
raise_on_missing_secret_store (bool, optional): Raise an error if the secret store declared in the connectorTestSuitesOptions is not available. Defaults to True.
85-
logger (logging.Logger | None, optional): Logger to log a warning if the secret store declared in the connectorTestSuitesOptions is not available. Defaults to None.
86-
87-
Raises:
88-
SecretNotFoundError: Raised if the secret store declared in the connectorTestSuitesOptions is not available and raise_on_missing_secret_store is truthy.
89-
90-
Returns:
91-
List[Secret]: List of secrets declared in the connectorTestSuitesOptions for a test suite name.
92-
"""
93-
secrets: List[Secret] = []
94-
enabled_test_suite = find(connector_test_suites_options, lambda x: x["suite"] == suite_name)
95-
96-
if enabled_test_suite and "testSecrets" in enabled_test_suite:
97-
for secret_info in enabled_test_suite["testSecrets"]:
98-
if secret := _process_secret(secret_info, secret_stores, raise_on_missing_secret_store, logger):
99-
secrets.append(secret)
100-
101-
return secrets
102-
103-
104-
def get_connector_secrets_for_test_suite(
105-
test_suite_name: str, context: ConnectorContext, connector_test_suites_options: List, local_secrets: List[Secret]
106-
) -> List[Secret]:
107-
"""Get secrets to use for a test suite.
108-
Always merge secrets declared in metadata's connectorTestSuiteOptions with secrets declared locally.
109-
110-
Args:
111-
test_suite_name (str): Name of the test suite to get secrets for
112-
context (ConnectorContext): The current connector context
113-
connector_test_suites_options (Dict): The current connector test suite options (from metadata)
114-
local_secrets (List[Secret]): The local connector secrets.
115-
116-
Returns:
117-
List[Secret]: Secrets to use to run the passed test suite name.
118-
"""
119-
return (
120-
get_secrets_from_connector_test_suites_option(
121-
connector_test_suites_options,
122-
test_suite_name,
123-
context.secret_stores,
124-
raise_on_missing_secret_store=context.is_ci,
125-
logger=context.logger,
126-
)
127-
+ local_secrets
128-
)
12930

13031

13132
class VersionCheck(Step, ABC):

0 commit comments

Comments
 (0)