Skip to content

Commit e48c12b

Browse files
author
dakodakov
authored
vdk-core: add vdk sdk secrets api - part III (#2325)
Incremental change for adding the secrets capability. Add secrets implementation and tests in vdk-core. --------- Signed-off-by: Dako Dakov <ddakov@vmware.com>
1 parent aba7b54 commit e48c12b

25 files changed

+779
-7
lines changed

projects/vdk-core/src/vdk/api/job_input.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class ISecrets:
4141
"""
4242

4343
@abstractmethod
44-
def get_secret(self, name: str, default_value: Any = None) -> str:
44+
def get_secret(self, key: str, default_value: Any = None) -> str:
4545
pass
4646

4747
@abstractmethod

projects/vdk-core/src/vdk/internal/builtin_plugins/builtin_hook_impl.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
from vdk.internal.builtin_plugins.job_properties.properties_api_plugin import (
3030
PropertiesApiPlugin,
3131
)
32+
from vdk.internal.builtin_plugins.job_secrets.secrets_api_plugin import (
33+
SecretsApiPlugin,
34+
)
3235
from vdk.internal.builtin_plugins.notification.notification import NotificationPlugin
3336
from vdk.internal.builtin_plugins.termination_message.writer import (
3437
TerminationMessageWriterPlugin,
@@ -119,6 +122,7 @@ def vdk_start(plugin_registry: PluginRegistry, command_line_args: List) -> None:
119122
plugin_registry.load_plugin_with_hooks_impl(NotificationPlugin())
120123
plugin_registry.load_plugin_with_hooks_impl(IngesterConfigurationPlugin())
121124
plugin_registry.load_plugin_with_hooks_impl(PropertiesApiPlugin())
125+
plugin_registry.load_plugin_with_hooks_impl(SecretsApiPlugin())
122126
# TODO: should be in run package only
123127
plugin_registry.add_hook_specs(JobRunHookSpecs)
124128
plugin_registry.load_plugin_with_hooks_impl(JobConfigIniPlugin())

projects/vdk-core/src/vdk/internal/builtin_plugins/job_properties/base_properties_impl.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# Copyright 2021-2023 VMware, Inc.
22
# SPDX-License-Identifier: Apache-2.0
3-
from typing import List
4-
from typing import Type
53

64

7-
def check_valid_property(k: str, v: str, supported_types: List[Type] = []) -> None:
5+
def check_valid_property(k: str, v: str, supported_types=None) -> None:
86
"""
97
Check if property key and value are valid
108
"""
9+
if supported_types is None:
10+
supported_types = []
11+
1112
if str != type(k) or k.strip() != k or "".join(k.split()) != k:
1213
msg = (
1314
f"Property {k} is of unsupported type or has unsupported name. "

projects/vdk-core/src/vdk/internal/builtin_plugins/job_properties/propertiesnotavailable.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ class PropertiesNotAvailable(IProperties):
1212

1313
def __init__(self, error_handler: Callable[[str], None]):
1414
self._error_handler = error_handler
15-
pass
1615

1716
def get_property(self, name, default_value=None): # @UnusedVariable
1817
self.tell_user("get_property")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright 2021-2023 VMware, Inc.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
5+
def check_valid_secret(k: str, v: str, supported_types=None) -> None:
6+
"""
7+
Check if secret key and value are valid
8+
"""
9+
10+
if supported_types is None:
11+
supported_types = []
12+
13+
if str != type(k) or k.strip() != k or "".join(k.split()) != k:
14+
msg = (
15+
f"Secret {k} is of unsupported type or has unsupported name. "
16+
f"Only string secrets with no whitespaces in the name are supported."
17+
)
18+
raise ValueError(msg)
19+
20+
if not supported_types:
21+
supported_types = [int, float, str, list, type(None)]
22+
23+
if type(v) not in supported_types:
24+
msg = (
25+
f"Value for secret {k} is of unsupported type {type(v)}. "
26+
f"Only int, float, str, list, and NoneType types are supported. "
27+
)
28+
raise ValueError(msg)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright 2021-2023 VMware, Inc.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from vdk.api.job_input import ISecrets
4+
5+
6+
class CachedSecretsWrapper(ISecrets):
7+
"""
8+
Wraps any ISecrets so that get_* calls are cached until set_* is called
9+
Since secrets are rarely updated, it's better to have them cached.
10+
"""
11+
12+
def __init__(self, secrets_impl: ISecrets):
13+
self.provider = secrets_impl
14+
self.cached_dict = (
15+
None # when None then cache needs to be refreshed on next get_*()
16+
)
17+
18+
def get_secret(self, name, default_value=None):
19+
res = self._get_cached_dict().get(name, default_value)
20+
return res
21+
22+
def get_all_secrets(self):
23+
return self._get_cached_dict()
24+
25+
def set_all_secrets(self, secrets):
26+
self.provider.set_all_secrets(secrets)
27+
self.cached_dict = None
28+
29+
def _get_cached_dict(self):
30+
if self.cached_dict is None:
31+
self.cached_dict = self.provider.get_all_secrets()
32+
return self.cached_dict
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Copyright 2021-2023 VMware, Inc.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import logging
4+
from copy import deepcopy
5+
from typing import Dict
6+
from typing import List
7+
from typing import Optional
8+
from typing import Union
9+
10+
from vdk.api.job_input import ISecrets
11+
from vdk.api.plugin.plugin_input import ISecretsServiceClient
12+
13+
from ...core.errors import log_and_throw
14+
from ...core.errors import ResolvableBy
15+
from .base_secrets_impl import check_valid_secret
16+
17+
log = logging.getLogger(__name__)
18+
19+
SecretValue = Union[int, float, str, list, dict, None]
20+
21+
22+
class DataJobsServiceSecrets(ISecrets):
23+
"""
24+
Data Jobs Secrets implementation.
25+
"""
26+
27+
__VALID_TYPES = [int, float, str, list, dict, type(None)]
28+
29+
def __init__(
30+
self,
31+
job_name: str,
32+
team_name: str,
33+
secrets_service_client: ISecretsServiceClient,
34+
write_preprocessors: Optional[List[ISecretsServiceClient]] = None,
35+
):
36+
log.debug(
37+
f"Data Job Secrets for job {job_name} with service client: {secrets_service_client}"
38+
)
39+
self._job_name = job_name
40+
self._team_name = team_name
41+
self._secrets_service_client = secrets_service_client
42+
self._write_preprocessors = write_preprocessors
43+
44+
def get_secret(self, name: str, default_value: SecretValue = None) -> SecretValue:
45+
"""
46+
:param name: The name of the secret
47+
:param default_value: default value ot return if missing
48+
"""
49+
secrets = self.get_all_secrets()
50+
if name in secrets:
51+
return secrets[name]
52+
else:
53+
log.warning(
54+
"Secret {} is not among Job secrets, returning default value: {}".format(
55+
name, default_value
56+
)
57+
)
58+
return default_value
59+
60+
def get_all_secrets(self) -> Dict[str, SecretValue]:
61+
"""
62+
:return: all stored secrets
63+
"""
64+
return self._secrets_service_client.read_secrets(
65+
self._job_name, self._team_name
66+
)
67+
68+
def set_all_secrets(self, secrets: Dict[str, SecretValue]) -> None:
69+
"""
70+
Invokes the write pre-processors if any are configured.
71+
Persists the passed secrets overwriting all previous properties.
72+
"""
73+
if self._write_preprocessors:
74+
secrets = deepcopy(
75+
secrets
76+
) # keeps the outer scope originally-referenced dictionary intact
77+
for client in self._write_preprocessors:
78+
try:
79+
secrets = client.write_secrets(
80+
self._job_name, self._team_name, secrets
81+
)
82+
except Exception as e:
83+
log_and_throw(
84+
to_be_fixed_by=ResolvableBy.USER_ERROR,
85+
log=log,
86+
what_happened=f"A write pre-processor of secrets client {client} had failed.",
87+
why_it_happened=f"User Error occurred. Exception was: {e}",
88+
consequences="SECRETS_WRITE_PREPROCESS_SEQUENCE was interrupted, and "
89+
"secrets won't be written by the SECRETS_DEFAULT_TYPE client.",
90+
countermeasures="Handle the exception raised.",
91+
)
92+
93+
for k, v in list(secrets.items()):
94+
check_valid_secret(k, v, DataJobsServiceSecrets.__VALID_TYPES) # throws
95+
96+
self._secrets_service_client.write_secrets(
97+
self._job_name, self._team_name, secrets
98+
)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright 2021-2023 VMware, Inc.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import logging
4+
from copy import deepcopy
5+
from typing import Dict
6+
7+
from vdk.api.plugin.plugin_input import ISecretsServiceClient
8+
9+
log = logging.getLogger(__name__)
10+
11+
12+
class InMemSecretsServiceClient(ISecretsServiceClient):
13+
"""
14+
Implementation of IProperties that are kept only in memory.
15+
"""
16+
17+
def __init__(self):
18+
self._secrets = {}
19+
20+
def read_secrets(self, job_name: str, team_name: str) -> Dict:
21+
res = deepcopy(self._secrets)
22+
return res
23+
24+
def write_secrets(self, job_name: str, team_name: str, secrets: Dict) -> Dict:
25+
log.warning(
26+
"You are using In Memory Secrets client. "
27+
"That means the secrets will not be persisted past the Data Job run."
28+
)
29+
self._secrets = deepcopy(secrets)
30+
return self._secrets
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2021-2023 VMware, Inc.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from vdk.api.plugin.hook_markers import hookimpl
4+
from vdk.internal.builtin_plugins.job_secrets import secrets_config
5+
from vdk.internal.builtin_plugins.job_secrets.inmemsecrets import (
6+
InMemSecretsServiceClient,
7+
)
8+
from vdk.internal.builtin_plugins.run.job_context import JobContext
9+
from vdk.internal.core.config import ConfigurationBuilder
10+
11+
12+
class SecretsApiPlugin:
13+
"""
14+
Define the basic configuration needed for Secrets API.
15+
"""
16+
17+
@hookimpl(tryfirst=True)
18+
def vdk_configure(self, config_builder: ConfigurationBuilder) -> None:
19+
secrets_config.add_definitions(config_builder)
20+
21+
@hookimpl
22+
def initialize_job(self, context: JobContext) -> None:
23+
context.secrets.set_secrets_factory_method("memory", InMemSecretsServiceClient)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright 2021-2023 VMware, Inc.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import List
4+
5+
from vdk.internal.core.config import Configuration
6+
from vdk.internal.core.config import ConfigurationBuilder
7+
from vdk.internal.util.utils import parse_config_sequence
8+
9+
SECRETS_DEFAULT_TYPE = "SECRETS_DEFAULT_TYPE"
10+
SECRETS_WRITE_PREPROCESS_SEQUENCE = "SECRETS_WRITE_PREPROCESS_SEQUENCE"
11+
12+
13+
class SecretsConfiguration:
14+
def __init__(self, config: Configuration):
15+
self.__config = config
16+
17+
def get_secrets_default_type(self) -> str:
18+
return self.__config.get_value(SECRETS_DEFAULT_TYPE)
19+
20+
def get_secrets_write_preprocess_sequence(self) -> List[str]:
21+
return parse_config_sequence(
22+
self.__config, key=SECRETS_WRITE_PREPROCESS_SEQUENCE, sep=","
23+
)
24+
25+
26+
def add_definitions(config_builder: ConfigurationBuilder):
27+
config_builder.add(
28+
key=SECRETS_DEFAULT_TYPE,
29+
default_value=None,
30+
description="Set the default secrets type to use. "
31+
"Plugins can register different secret types. "
32+
"This option controls which is in use"
33+
"It can be left empty, in which case "
34+
"if there is only one type registered it will use it."
35+
"Or it will use one register with type 'default' ",
36+
)
37+
config_builder.add(
38+
key=SECRETS_WRITE_PREPROCESS_SEQUENCE,
39+
default_value=None,
40+
description="""A string of comma-separated secret types.
41+
Those types are priorly registered in the ISecretsRegistry, by
42+
mapping a factory for instantiating each ISecretsServiceClient
43+
secrets type handler.
44+
This comma-separated string value indicates the sequence in which those
45+
ISecretsServiceClient implementations `write_secrets` method
46+
will be invoked. For example:
47+
SECRETS_WRITE_PREPROCESS_SEQUENCE="a-prefixed-secret,
48+
replicated-secret"
49+
would mean that the secrets data stored would be first
50+
processed by the `a-prefixed-secret` client, then by the
51+
`replicated-secret` client, and finally would be stored by
52+
the default secret client.
53+
In case of pre-processing failure, the default client won't be invoked.
54+
""",
55+
)

0 commit comments

Comments
 (0)