in metaflow/plugins/gcp/gcp_secret_manager_secrets_provider.py [0:0]
def get_secret_as_dict(self, secret_id, options={}, role=None):
"""
Reads a secret from GCP Secrets Manager and returns it as a dictionary of environment variables.
If the secret contains a string payload ("SecretString"):
- if the `json` option is True:
Secret will be parsed as a JSON. If successfully parsed, AND the JSON contains a
top-level object, each entry K/V in the object will also be converted to an entry in the result. V will
always be casted to a string (if not already a string).
- If `json` option is False (default):
Will be returned as a single entry in the result, with the key being the last part after / in secret_id.
On GCP Secrets Manager, the secret payload is a binary blob. However, by default we interpret it as UTF8 encoded
string. To disable this, set the `binary` option to True, the binary will be base64 encoded in the result.
All keys in the result are sanitized to be more valid environment variable names. This is done on a best effort
basis. Further validation is expected to be done by the invoking @secrets decorator itself.
:param secret_id: GCP Secrets Manager secret ID
:param options: unused
:return: dict of environment variables. All keys and values are strings.
"""
from google.cloud.secretmanager_v1.services.secret_manager_service import (
SecretManagerServiceClient,
)
from google.cloud.secretmanager_v1.services.secret_manager_service.transports import (
SecretManagerServiceTransport,
)
# Full secret id looks like projects/1234567890/secrets/mysecret/versions/latest
#
# We allow these forms of secret_id:
#
# 1. Full path like projects/1234567890/secrets/mysecret/versions/latest
# This is what you'd specify if you used to GCP SDK.
#
# 2. Full path but without the version like projects/1234567890/secrets/mysecret.
# This is what you see in the GCP console, makes it easier to copy & paste.
#
# 3. Simple string like mysecret
#
# 4. Simple string with /versions/<version> suffix like mysecret/versions/1
# The latter two forms require METAFLOW_GCP_SECRET_MANAGER_PREFIX to be set.
match_full = re.match(
r"^projects/\d+/secrets/([\w\-]+)(/versions/([\w\-]+))?$", secret_id
)
match_partial = re.match(r"^([\w\-]+)(/versions/[\w\-]+)?$", secret_id)
if match_full:
# Full path
env_var_name = match_full.group(1)
if match_full.group(3):
# With version specified
full_secret_name = secret_id
else:
# No version specified, use latest
full_secret_name = secret_id + "/versions/latest"
elif match_partial:
# Partial path, possibly with /versions/<version> suffix
env_var_name = secret_id
if not GCP_SECRET_MANAGER_PREFIX:
raise ValueError(
"Cannot use simple secret_id without setting METAFLOW_GCP_SECRET_MANAGER_PREFIX. %s"
% GCP_SECRET_MANAGER_PREFIX
)
if match_partial.group(2):
# With version specified
full_secret_name = "%s%s" % (GCP_SECRET_MANAGER_PREFIX, secret_id)
env_var_name = match_partial.group(1)
else:
# No version specified, use latest
full_secret_name = "%s%s/versions/latest" % (
GCP_SECRET_MANAGER_PREFIX,
secret_id,
)
else:
raise ValueError(
"Invalid secret_id: %s. Must be either a full path or a simple string."
% secret_id
)
result = {}
def _sanitize_and_add_entry_to_result(k, v):
# Two jobs - sanitize, and check for dupes
sanitized_k = _sanitize_key_as_env_var(k)
if sanitized_k in result:
raise MetaflowGcpSecretsManagerDuplicateKey(
"Duplicate key in secret: '%s' (sanitizes to '%s')"
% (k, sanitized_k)
)
result[sanitized_k] = v
credentials, _ = get_credentials(
scopes=SecretManagerServiceTransport.AUTH_SCOPES
)
client = SecretManagerServiceClient(credentials=credentials)
response = client.access_secret_version(request={"name": full_secret_name})
payload_str = response.payload.data.decode("UTF-8")
if options.get("json", False):
obj = json.loads(payload_str)
if type(obj) == dict:
for k, v in obj.items():
# We try to make it work here - cast to string always
_sanitize_and_add_entry_to_result(k, str(v))
else:
raise MetaflowGcpSecretsManagerNotJSONObject(
"Secret string is a JSON, but not an object (dict-like) - actual type %s."
% type(obj)
)
else:
if options.get("env_var_name"):
env_var_name = options["env_var_name"]
if options.get("binary", False):
_sanitize_and_add_entry_to_result(
env_var_name, base64.b64encode(response.payload.data)
)
else:
_sanitize_and_add_entry_to_result(env_var_name, payload_str)
return result