Skip to content

Commit dd94c20

Browse files
Support environment variable interpolation in keyvault config (#504)
* modify SafeLoaderIgnoreUnknown to expand keyvalut env vars * add KEYVAULT_NAME secrets to github workflow * add testcases
1 parent ac0840a commit dd94c20

File tree

5 files changed

+104
-17
lines changed

5 files changed

+104
-17
lines changed

.github/workflows/test_and_build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
KEYVAULT_CLIENT_ID: ${{ secrets.KEYVAULT_CLIENT_ID }}
4343
KEYVAULT_TENANT_ID: ${{ secrets.KEYVAULT_TENANT_ID }}
4444
KEYVAULT_CLIENT_SECRET: ${{ secrets.KEYVAULT_CLIENT_SECRET }}
45+
KEYVAULT_NAME: ${{ secrets.KEYVAULT_NAME }}
4546
COGNITE_PROJECT: extractor-tests
4647
COGNITE_BASE_URL: https://greenfield.cognitedata.com
4748
COGNITE_DEV_PROJECT: extractor-aws-dub-dev-testing

cognite/extractorutils/configtools/loaders.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ def __init__(self, config: dict | None) -> None:
7878
self.client: SecretClient | None = None
7979

8080
def _init_client(self) -> None:
81-
from dotenv import load_dotenv
82-
8381
if not self.config:
8482
raise InvalidConfigError(
8583
"Attempted to load values from Azure key vault with no key vault configured. "
@@ -108,20 +106,11 @@ def _init_client(self) -> None:
108106

109107
_logger.info("Using Azure ClientSecret credentials to access KeyVault")
110108

111-
env_file_found = load_dotenv("./.env", override=True)
112-
113-
if not env_file_found:
114-
_logger.info(f"Local environment file not found at {Path.cwd() / '.env'}")
115-
116109
if all(param in self.config for param in auth_parameters):
117-
tenant_id = os.path.expandvars(self.config["tenant-id"])
118-
client_id = os.path.expandvars(self.config["client-id"])
119-
secret = os.path.expandvars(self.config["secret"])
120-
121110
credentials = ClientSecretCredential(
122-
tenant_id=tenant_id,
123-
client_id=client_id,
124-
client_secret=secret,
111+
tenant_id=self.config["tenant-id"],
112+
client_id=self.config["client-id"],
113+
client_secret=self.config["secret"],
125114
)
126115
else:
127116
raise InvalidConfigError(
@@ -184,6 +173,10 @@ def ignore_unknown(self, node: yaml.Node) -> None:
184173
# Ignoring types since the key can be None.
185174

186175
SafeLoaderIgnoreUnknown.add_constructor(None, SafeLoaderIgnoreUnknown.ignore_unknown) # type: ignore
176+
if expand_envvars:
177+
SafeLoaderIgnoreUnknown.add_implicit_resolver("!env", re.compile(r"\$\{([^}^{]+)\}"), None)
178+
SafeLoaderIgnoreUnknown.add_constructor("!env", _env_constructor)
179+
187180
initial_load = yaml.load(source, Loader=SafeLoaderIgnoreUnknown) # noqa: S506
188181

189182
if not isinstance(initial_load, dict):

tests/tests_integration/dummyconfig_keyvault_remote.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ azure-keyvault:
55
client-id: ${KEYVAULT_CLIENT_ID}
66
tenant-id: ${KEYVAULT_TENANT_ID}
77
secret: ${KEYVAULT_CLIENT_SECRET}
8-
keyvault-name: extractor-keyvault
8+
keyvault-name: ${KEYVAULT_NAME}
99

1010
cognite:
1111
host: ${COGNITE_BASE_URL}

tests/tests_unit/dummyconfig_keyvault.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ azure-keyvault:
99
client-id: ${KEYVAULT_CLIENT_ID}
1010
tenant-id: ${KEYVAULT_TENANT_ID}
1111
secret: ${KEYVAULT_CLIENT_SECRET}
12-
keyvault-name: extractor-keyvault
12+
keyvault-name: ${KEYVAULT_NAME}
1313

1414
cognite:
1515
project: mathiaslohne-develop

tests/tests_unit/test_configtools.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from dataclasses import dataclass
2323
from pathlib import Path
2424
from typing import IO
25-
from unittest.mock import patch
25+
from unittest.mock import MagicMock, patch
2626

2727
import pytest
2828
import yaml
@@ -51,6 +51,7 @@
5151
from cognite.extractorutils.configtools.loaders import (
5252
ConfigResolver,
5353
compile_patterns,
54+
load_yaml_dict,
5455
)
5556
from cognite.extractorutils.configtools.validators import matches_pattern, matches_patterns
5657
from cognite.extractorutils.exceptions import InvalidConfigError
@@ -750,3 +751,95 @@ def test_configresolver_fallback_encoding(tmp_path: Path, caplog: pytest.LogCapt
750751
assert config.logger.file.path is not None
751752
assert "café" in config.logger.file.path
752753
assert any("Falling back to system default encoding." in r.message for r in caplog.records)
754+
755+
756+
@pytest.mark.parametrize("auth_method", ["default", "client-secret"])
757+
def test_keyvault_config_env_var_expansion(monkeypatch: pytest.MonkeyPatch, auth_method: str) -> None:
758+
monkeypatch.setenv("MY_KEYVAULT_NAME", "test-keyvault-from-env")
759+
760+
if auth_method == "default":
761+
yaml_config = """
762+
azure-keyvault:
763+
keyvault-name: ${MY_KEYVAULT_NAME}
764+
authentication-method: default
765+
766+
database:
767+
password: !keyvault db-password
768+
"""
769+
else:
770+
monkeypatch.setenv("KV_CLIENT_ID", "client-id-123")
771+
monkeypatch.setenv("KV_TENANT_ID", "tenant-id-456")
772+
monkeypatch.setenv("KV_SECRET", "secret-789")
773+
yaml_config = """
774+
azure-keyvault:
775+
keyvault-name: ${MY_KEYVAULT_NAME}
776+
authentication-method: client-secret
777+
client-id: ${KV_CLIENT_ID}
778+
tenant-id: ${KV_TENANT_ID}
779+
secret: ${KV_SECRET}
780+
781+
database:
782+
password: !keyvault db-password
783+
"""
784+
785+
with (
786+
patch("cognite.extractorutils.configtools.loaders.DefaultAzureCredential") as mock_default_cred,
787+
patch("cognite.extractorutils.configtools.loaders.ClientSecretCredential") as mock_client_cred,
788+
patch("cognite.extractorutils.configtools.loaders.SecretClient") as mock_secret_client,
789+
):
790+
mock_client_instance = MagicMock()
791+
mock_client_instance.get_secret.return_value = MagicMock(value="secret-from-keyvault")
792+
mock_secret_client.return_value = mock_client_instance
793+
mock_default_cred.return_value = MagicMock()
794+
mock_client_cred.return_value = MagicMock()
795+
796+
config = load_yaml_dict(yaml_config)
797+
798+
mock_secret_client.assert_called_once()
799+
call_kwargs = mock_secret_client.call_args[1]
800+
assert call_kwargs["vault_url"] == "https://test-keyvault-from-env.vault.azure.net"
801+
802+
assert config["database"]["password"] == "secret-from-keyvault"
803+
804+
if auth_method == "default":
805+
mock_default_cred.assert_called_once()
806+
mock_client_cred.assert_not_called()
807+
808+
if auth_method == "client-secret":
809+
mock_client_cred.assert_called_once_with(
810+
tenant_id="tenant-id-456",
811+
client_id="client-id-123",
812+
client_secret="secret-789",
813+
)
814+
mock_default_cred.assert_not_called()
815+
816+
817+
def test_keyvault_tag_without_config_raises_error() -> None:
818+
yaml_config = """
819+
database:
820+
password: !keyvault db-password
821+
"""
822+
823+
with pytest.raises(InvalidConfigError) as e:
824+
load_yaml_dict(yaml_config)
825+
assert (
826+
e.value.message
827+
== "Attempted to load values from Azure key vault with no key vault configured. Include an `azure-keyvault` section in your config to use the !keyvault tag."
828+
)
829+
830+
831+
def test_keyvault_client_secret_missing_raises_error() -> None:
832+
yaml_config = """
833+
azure-keyvault:
834+
keyvault-name: test-keyvault-from-env
835+
authentication-method: client-secret
836+
client-id: client-id-123
837+
tenant-id: tenant-id-456
838+
839+
database:
840+
password: !keyvault db-password
841+
"""
842+
843+
with pytest.raises(InvalidConfigError) as e:
844+
load_yaml_dict(yaml_config)
845+
assert e.value.message == "Missing client secret parameters. client-id, tenant-id and client-secret are mandatory"

0 commit comments

Comments
 (0)